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": {
|
"dependencies": {
|
||||||
"bulma": "^0.7.1",
|
"bulma": "^0.7.1",
|
||||||
|
"lodash": "^4.17.11",
|
||||||
"v-tooltip": "^2.0.0-rc.33",
|
"v-tooltip": "^2.0.0-rc.33",
|
||||||
"vue": "^2.5.17"
|
"vue": "^2.5.17"
|
||||||
},
|
},
|
||||||
|
|
20
src/App.vue
20
src/App.vue
|
@ -70,7 +70,7 @@
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<aside class="menu namespaces-lists">
|
<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">
|
<template v-for="n in namespaces">
|
||||||
<div :key="n.id">
|
<div :key="n.id">
|
||||||
<router-link v-tooltip.right="'Settings'" :to="{name: 'editNamespace', params: {id: n.id} }" class="nsettings" v-if="n.id > 0">
|
<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>
|
<script>
|
||||||
import auth from './auth'
|
import auth from './auth'
|
||||||
import {HTTP} from './http-common'
|
|
||||||
import message from './message'
|
import message from './message'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
import NamespaceService from './services/namespace'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'app',
|
name: 'app',
|
||||||
|
@ -127,7 +127,6 @@
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
user: auth.user,
|
user: auth.user,
|
||||||
loading: false,
|
|
||||||
namespaces: [],
|
namespaces: [],
|
||||||
mobileMenuActive: false,
|
mobileMenuActive: false,
|
||||||
fullpage: false,
|
fullpage: false,
|
||||||
|
@ -165,15 +164,13 @@
|
||||||
return 'https://www.gravatar.com/avatar/' + this.user.infos.avatar + '?s=50'
|
return 'https://www.gravatar.com/avatar/' + this.user.infos.avatar + '?s=50'
|
||||||
},
|
},
|
||||||
loadNamespaces() {
|
loadNamespaces() {
|
||||||
const cancel = message.setLoading(this)
|
let namespaceService = new NamespaceService()
|
||||||
HTTP.get(`namespaces`, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
namespaceService.getAll()
|
||||||
.then(response => {
|
.then(r => {
|
||||||
this.$set(this, 'namespaces', response.data)
|
this.$set(this, 'namespaces', r)
|
||||||
cancel()
|
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
cancel()
|
message.error(e, this)
|
||||||
this.handleError(e)
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
loadNamespacesIfNeeded(e){
|
loadNamespacesIfNeeded(e){
|
||||||
|
@ -189,9 +186,6 @@
|
||||||
setFullPage() {
|
setFullPage() {
|
||||||
this.fullpage = true;
|
this.fullpage = true;
|
||||||
},
|
},
|
||||||
handleError(e) {
|
|
||||||
message.error(e, this)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -11,13 +11,14 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
login(context, creds, redirect) {
|
login(context, creds, redirect) {
|
||||||
|
localStorage.removeItem('token') // Delete an eventually preexisting old token
|
||||||
|
|
||||||
HTTP.post('login', {
|
HTTP.post('login', {
|
||||||
username: creds.username,
|
username: creds.username,
|
||||||
password: creds.password
|
password: creds.password
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
// Save the token to local storage for later use
|
// Save the token to local storage for later use
|
||||||
localStorage.removeItem('token') // Delete an eventually preexisting old token
|
|
||||||
localStorage.setItem('token', response.data.token)
|
localStorage.setItem('token', response.data.token)
|
||||||
|
|
||||||
// Tell others the user is autheticated
|
// Tell others the user is autheticated
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="loader-container" :class="{ 'is-loading': loading}">
|
<div class="loader-container" :class="{ 'is-loading': listService.loading}">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<header class="card-header">
|
<header class="card-header">
|
||||||
<p class="card-header-title">
|
<p class="card-header-title">
|
||||||
|
@ -12,25 +12,25 @@
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="listtext">List Name</label>
|
<label class="label" for="listtext">List Name</label>
|
||||||
<div class="control">
|
<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>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="listdescription">Description</label>
|
<label class="label" for="listdescription">Description</label>
|
||||||
<div class="control">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="columns bigbuttons">
|
<div class="columns bigbuttons">
|
||||||
<div class="column">
|
<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
|
Save
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-1">
|
<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">
|
<span class="icon is-small">
|
||||||
<icon icon="trash-alt"/>
|
<icon icon="trash-alt"/>
|
||||||
</span>
|
</span>
|
||||||
|
@ -41,9 +41,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<manageusers :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>
|
||||||
<manageteams :id="list.id" type="list" :userIsAdmin="userIsAdmin" />
|
|
||||||
|
|
||||||
<modal
|
<modal
|
||||||
v-if="showDeleteModal"
|
v-if="showDeleteModal"
|
||||||
|
@ -59,21 +58,25 @@
|
||||||
<script>
|
<script>
|
||||||
import auth from '../../auth'
|
import auth from '../../auth'
|
||||||
import router from '../../router'
|
import router from '../../router'
|
||||||
import {HTTP} from '../../http-common'
|
|
||||||
import message from '../../message'
|
import message from '../../message'
|
||||||
import manageusers from '../sharing/user'
|
import manageusers from '../sharing/user'
|
||||||
import manageteams from '../sharing/team'
|
import manageteams from '../sharing/team'
|
||||||
|
import ListModel from '../../models/list'
|
||||||
|
import ListService from '../../services/list'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "EditList",
|
name: "EditList",
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
list: {title: '', description:''},
|
list: ListModel,
|
||||||
error: '',
|
listService: ListService,
|
||||||
loading: false,
|
|
||||||
showDeleteModal: false,
|
showDeleteModal: false,
|
||||||
user: auth.user,
|
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: {
|
components: {
|
||||||
|
@ -85,10 +88,9 @@
|
||||||
if (!auth.user.authenticated) {
|
if (!auth.user.authenticated) {
|
||||||
router.push({name: 'home'})
|
router.push({name: 'home'})
|
||||||
}
|
}
|
||||||
|
|
||||||
this.list.id = this.$route.params.id
|
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
this.listService = new ListService()
|
||||||
this.loadList()
|
this.loadList()
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -97,61 +99,49 @@
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
loadList() {
|
loadList() {
|
||||||
const cancel = message.setLoading(this)
|
let list = new ListModel({id: this.$route.params.id})
|
||||||
|
this.listService.get(list)
|
||||||
HTTP.get(`lists/` + this.$route.params.id, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
.then(r => {
|
||||||
.then(response => {
|
this.$set(this, 'list', r)
|
||||||
this.$set(this, 'list', response.data)
|
if (r.owner.id === this.user.infos.id) {
|
||||||
if (response.data.owner.id === this.user.infos.id) {
|
|
||||||
this.userIsAdmin = true
|
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 => {
|
.catch(e => {
|
||||||
this.handleError(e)
|
message.error(e, this)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
submit() {
|
submit() {
|
||||||
const cancel = message.setLoading(this)
|
this.listService.update(this.list)
|
||||||
|
.then(r => {
|
||||||
HTTP.post(`lists/` + this.$route.params.id, this.list, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
|
||||||
.then(response => {
|
|
||||||
// Update the list in the parent
|
// Update the list in the parent
|
||||||
for (const n in this.$parent.namespaces) {
|
for (const n in this.$parent.namespaces) {
|
||||||
let lists = this.$parent.namespaces[n].lists
|
let lists = this.$parent.namespaces[n].lists
|
||||||
for (const l in lists) {
|
for (const l in lists) {
|
||||||
if (lists[l].id === response.data.id) {
|
if (lists[l].id === r.id) {
|
||||||
this.$set(this.$parent.namespaces[n].lists, l, response.data)
|
this.$set(this.$parent.namespaces[n].lists, l, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.handleSuccess({message: 'The list was successfully updated.'})
|
message.success({message: 'The list was successfully updated.'}, this)
|
||||||
cancel()
|
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
cancel()
|
message.error(e, this)
|
||||||
this.handleError(e)
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
deleteList() {
|
deleteList() {
|
||||||
const cancel = message.setLoading(this)
|
this.listService.delete(this.list)
|
||||||
HTTP.delete(`lists/` + this.$route.params.id, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.handleSuccess({message: 'The list was successfully deleted.'})
|
message.success({message: 'The list was successfully deleted.'}, this)
|
||||||
cancel()
|
|
||||||
router.push({name: 'home'})
|
router.push({name: 'home'})
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
cancel()
|
message.error(e, this)
|
||||||
this.handleError(e)
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
handleError(e) {
|
|
||||||
message.error(e, this)
|
|
||||||
},
|
|
||||||
handleSuccess(e) {
|
|
||||||
message.success(e, this)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -7,8 +7,8 @@
|
||||||
<h3>Create a new list</h3>
|
<h3>Create a new list</h3>
|
||||||
<form @submit.prevent="newList" @keyup.esc="back()">
|
<form @submit.prevent="newList" @keyup.esc="back()">
|
||||||
<div class="field is-grouped">
|
<div class="field is-grouped">
|
||||||
<p class="control is-expanded" :class="{ 'is-loading': loading}">
|
<p class="control is-expanded" :class="{ 'is-loading': listService.loading}">
|
||||||
<input v-focus class="input" :class="{ 'disabled': loading}" v-model="list.title" type="text" placeholder="The list's name goes here...">
|
<input v-focus class="input" :class="{ 'disabled': listService.loading}" v-model="list.title" type="text" placeholder="The list's name goes here...">
|
||||||
</p>
|
</p>
|
||||||
<p class="control">
|
<p class="control">
|
||||||
<button type="submit" class="button is-success noshadow">
|
<button type="submit" class="button is-success noshadow">
|
||||||
|
@ -26,16 +26,16 @@
|
||||||
<script>
|
<script>
|
||||||
import auth from '../../auth'
|
import auth from '../../auth'
|
||||||
import router from '../../router'
|
import router from '../../router'
|
||||||
import {HTTP} from '../../http-common'
|
|
||||||
import message from '../../message'
|
import message from '../../message'
|
||||||
|
import ListService from '../../services/list'
|
||||||
|
import ListModel from '../../models/list'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "NewList",
|
name: "NewList",
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
list: {title: ''},
|
list: ListModel,
|
||||||
error: '',
|
listService: ListService,
|
||||||
loading: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeMount() {
|
beforeMount() {
|
||||||
|
@ -45,33 +45,26 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
this.list = new ListModel()
|
||||||
|
this.listService = new ListService()
|
||||||
this.$parent.setFullPage();
|
this.$parent.setFullPage();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
newList() {
|
newList() {
|
||||||
const cancel = message.setLoading(this)
|
this.list.namespaceID = this.$route.params.id
|
||||||
|
this.listService.create(this.list)
|
||||||
HTTP.put(`namespaces/` + this.$route.params.id + `/lists`, this.list, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
|
||||||
.then(response => {
|
.then(response => {
|
||||||
this.$parent.loadNamespaces()
|
this.$parent.loadNamespaces()
|
||||||
this.handleSuccess({message: 'The list was successfully created.'})
|
message.success({message: 'The list was successfully created.'}, this)
|
||||||
cancel()
|
router.push({name: 'showList', params: {id: response.id}})
|
||||||
router.push({name: 'showList', params: {id: response.data.id}})
|
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
cancel()
|
message.error(e, this)
|
||||||
this.handleError(e)
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
back() {
|
back() {
|
||||||
router.go(-1)
|
router.go(-1)
|
||||||
},
|
},
|
||||||
handleError(e) {
|
|
||||||
message.error(e, this)
|
|
||||||
},
|
|
||||||
handleSuccess(e) {
|
|
||||||
message.success(e, this)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="loader-container" :class="{ 'is-loading': loading}">
|
<div class="loader-container" :class="{ 'is-loading': listService.loading}">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<router-link :to="{ name: 'editList', params: { id: list.id } }" class="icon settings is-medium">
|
<router-link :to="{ name: 'editList', params: { id: list.id } }" class="icon settings is-medium">
|
||||||
<icon icon="cog" size="2x"/>
|
<icon icon="cog" size="2x"/>
|
||||||
|
@ -8,8 +8,8 @@
|
||||||
</div>
|
</div>
|
||||||
<form @submit.prevent="addTask()">
|
<form @submit.prevent="addTask()">
|
||||||
<div class="field is-grouped">
|
<div class="field is-grouped">
|
||||||
<p class="control has-icons-left is-expanded" :class="{ 'is-loading': loading}">
|
<p class="control has-icons-left is-expanded" :class="{ 'is-loading': taskService.loading}">
|
||||||
<input v-focus class="input" :class="{ 'disabled': loading}" v-model="newTask.text" type="text" placeholder="Add a new task...">
|
<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">
|
<span class="icon is-small is-left">
|
||||||
<icon icon="tasks"/>
|
<icon icon="tasks"/>
|
||||||
</span>
|
</span>
|
||||||
|
@ -68,21 +68,21 @@
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="tasktext">Task Text</label>
|
<label class="label" for="tasktext">Task Text</label>
|
||||||
<div class="control">
|
<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>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="taskdescription">Description</label>
|
<label class="label" for="taskdescription">Description</label>
|
||||||
<div class="control">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<b>Reminder Dates</b>
|
<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">
|
<div class="reminder-input" :class="{ 'overdue': (r < nowUnix && index !== (taskEditTask.reminderDates.length - 1))}" v-for="(r, index) in taskEditTask.reminderDates" :key="index">
|
||||||
<flat-pickr
|
<flat-pickr
|
||||||
:class="{ 'disabled': loading}"
|
:class="{ 'disabled': taskService.loading}"
|
||||||
:disabled="loading"
|
:disabled="taskService.loading"
|
||||||
:v-model="taskEditTask.reminderDates"
|
:v-model="taskEditTask.reminderDates"
|
||||||
:config="flatPickerConfig"
|
:config="flatPickerConfig"
|
||||||
:id="'taskreminderdate' + index"
|
:id="'taskreminderdate' + index"
|
||||||
|
@ -97,9 +97,9 @@
|
||||||
<label class="label" for="taskduedate">Due Date</label>
|
<label class="label" for="taskduedate">Due Date</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<flat-pickr
|
<flat-pickr
|
||||||
:class="{ 'disabled': loading}"
|
:class="{ 'disabled': taskService.loading}"
|
||||||
class="input"
|
class="input"
|
||||||
:disabled="loading"
|
:disabled="taskService.loading"
|
||||||
v-model="taskEditTask.dueDate"
|
v-model="taskEditTask.dueDate"
|
||||||
:config="flatPickerConfig"
|
:config="flatPickerConfig"
|
||||||
id="taskduedate"
|
id="taskduedate"
|
||||||
|
@ -113,9 +113,9 @@
|
||||||
<div class="control columns">
|
<div class="control columns">
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<flat-pickr
|
<flat-pickr
|
||||||
:class="{ 'disabled': loading}"
|
:class="{ 'disabled': taskService.loading}"
|
||||||
class="input"
|
class="input"
|
||||||
:disabled="loading"
|
:disabled="taskService.loading"
|
||||||
v-model="taskEditTask.startDate"
|
v-model="taskEditTask.startDate"
|
||||||
:config="flatPickerConfig"
|
:config="flatPickerConfig"
|
||||||
id="taskduedate"
|
id="taskduedate"
|
||||||
|
@ -124,9 +124,9 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<flat-pickr
|
<flat-pickr
|
||||||
:class="{ 'disabled': loading}"
|
:class="{ 'disabled': taskService.loading}"
|
||||||
class="input"
|
class="input"
|
||||||
:disabled="loading"
|
:disabled="taskService.loading"
|
||||||
v-model="taskEditTask.endDate"
|
v-model="taskEditTask.endDate"
|
||||||
:config="flatPickerConfig"
|
:config="flatPickerConfig"
|
||||||
id="taskduedate"
|
id="taskduedate"
|
||||||
|
@ -140,11 +140,11 @@
|
||||||
<label class="label" for="">Repeat after</label>
|
<label class="label" for="">Repeat after</label>
|
||||||
<div class="control repeat-after-input columns">
|
<div class="control repeat-after-input columns">
|
||||||
<div class="column">
|
<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>
|
||||||
<div class="column is-3">
|
<div class="column is-3">
|
||||||
<div class="select">
|
<div class="select">
|
||||||
<select v-model="repeatAfter.type">
|
<select v-model="taskEditTask.repeatAfter.type">
|
||||||
<option value="hours">Hours</option>
|
<option value="hours">Hours</option>
|
||||||
<option value="days">Days</option>
|
<option value="days">Days</option>
|
||||||
<option value="weeks">Weeks</option>
|
<option value="weeks">Weeks</option>
|
||||||
|
@ -179,14 +179,14 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="field has-addons">
|
<div class="field has-addons">
|
||||||
<div class="control is-expanded">
|
<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>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<a class="button" @click="addSubtask()"><icon icon="plus"></icon></a>
|
<a class="button" @click="addSubtask()"><icon icon="plus"></icon></a>
|
||||||
</div>
|
</div>
|
||||||
</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
|
Save
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
@ -202,26 +202,30 @@
|
||||||
<script>
|
<script>
|
||||||
import auth from '../../auth'
|
import auth from '../../auth'
|
||||||
import router from '../../router'
|
import router from '../../router'
|
||||||
import {HTTP} from '../../http-common'
|
|
||||||
import message from '../../message'
|
import message from '../../message'
|
||||||
import flatPickr from 'vue-flatpickr-component';
|
import flatPickr from 'vue-flatpickr-component'
|
||||||
import 'flatpickr/dist/flatpickr.css';
|
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 {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
listID: this.$route.params.id,
|
listID: this.$route.params.id,
|
||||||
|
listService: ListService,
|
||||||
|
taskService: TaskService,
|
||||||
|
|
||||||
list: {},
|
list: {},
|
||||||
newTask: {text: ''},
|
newTask: TaskModel,
|
||||||
error: '',
|
|
||||||
loading: false,
|
|
||||||
isTaskEdit: false,
|
isTaskEdit: false,
|
||||||
taskEditTask: {
|
taskEditTask: {
|
||||||
subtasks: [],
|
subtasks: [],
|
||||||
},
|
},
|
||||||
lastReminder: 0,
|
lastReminder: 0,
|
||||||
nowUnix: new Date(),
|
nowUnix: new Date(),
|
||||||
repeatAfter: {type: 'days', amount: null},
|
|
||||||
flatPickerConfig:{
|
flatPickerConfig:{
|
||||||
altFormat: 'j M Y H:i',
|
altFormat: 'j M Y H:i',
|
||||||
altInput: true,
|
altInput: true,
|
||||||
|
@ -242,6 +246,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
this.listService = new ListService()
|
||||||
|
this.taskService = new TaskService()
|
||||||
|
this.newTask = new TaskModel()
|
||||||
this.loadList()
|
this.loadList()
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -251,243 +258,89 @@
|
||||||
methods: {
|
methods: {
|
||||||
loadList() {
|
loadList() {
|
||||||
this.isTaskEdit = false
|
this.isTaskEdit = false
|
||||||
const cancel = message.setLoading(this)
|
// 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})
|
||||||
HTTP.get(`lists/` + this.$route.params.id, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
this.listService.get(list)
|
||||||
.then(response => {
|
.then(r => {
|
||||||
for (const t in response.data.tasks) {
|
this.$set(this, 'list', r)
|
||||||
response.data.tasks[t] = this.fixStuffComingFromAPI(response.data.tasks[t])
|
|
||||||
}
|
|
||||||
|
|
||||||
// This adds a new elemednt "list" to our object which contains all lists
|
|
||||||
response.data.tasks = this.sortTasks(response.data.tasks)
|
|
||||||
this.$set(this, 'list', response.data)
|
|
||||||
if (this.list.tasks === null) {
|
|
||||||
this.list.tasks = []
|
|
||||||
}
|
|
||||||
cancel() // cancel the timer
|
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
cancel()
|
message.error(e, this)
|
||||||
this.handleError(e)
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
addTask() {
|
addTask() {
|
||||||
const cancel = message.setLoading(this)
|
this.newTask.listID = this.$route.params.id
|
||||||
|
this.taskService.create(this.newTask)
|
||||||
HTTP.put(`lists/` + this.$route.params.id, this.newTask, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
.then(r => {
|
||||||
.then(response => {
|
this.list.addTaskToList(r)
|
||||||
this.addTaskToList(response.data)
|
message.success({message: 'The task was successfully created.'}, this)
|
||||||
this.handleSuccess({message: 'The task was successfully created.'})
|
|
||||||
cancel() // cancel the timer
|
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
cancel()
|
message.error(e, this)
|
||||||
this.handleError(e)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
this.newTask = {}
|
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) {
|
markAsDone(e) {
|
||||||
let context = this
|
let updateFunc = () => {
|
||||||
if (e.target.checked) {
|
// We get the task, update the 'done' property and then push it to the api.
|
||||||
setTimeout(doTheDone, 300); // Delay it to show the animation when marking a task as done
|
let task = this.list.getTaskByID(e.target.id)
|
||||||
} else {
|
task.done = e.target.checked
|
||||||
doTheDone() // Don't delay it when un-marking it as it doesn't have an animation the other way around
|
this.taskService.update(task)
|
||||||
}
|
.then(r => {
|
||||||
|
this.updateTaskInList(r)
|
||||||
function doTheDone() {
|
message.success({message: 'The task was successfully ' + (task.done ? '' : 'un-') + 'marked as done.'}, this)
|
||||||
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.
|
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
cancel()
|
message.error(e, this)
|
||||||
context.handleError(e)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
editTask(id) {
|
||||||
// Find the selected task and set it to the current object
|
// Find the selected task and set it to the current object
|
||||||
for (const t in this.list.tasks) {
|
let theTask = this.list.getTaskByID(id) // Somehow this does not work if we directly assign this to this.taskEditTask
|
||||||
if (this.list.tasks[t].id === id) {
|
this.taskEditTask = theTask
|
||||||
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 = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isTaskEdit = true
|
this.isTaskEdit = true
|
||||||
},
|
},
|
||||||
editTaskSubmit() {
|
editTaskSubmit() {
|
||||||
const cancel = message.setLoading(this)
|
this.taskService.update(this.taskEditTask)
|
||||||
|
.then(r => {
|
||||||
// Convert the date in a unix timestamp
|
this.updateTaskInList(r)
|
||||||
this.taskEditTask.dueDate = (+ new Date(this.taskEditTask.dueDate)) / 1000
|
this.$set(this, 'taskEditTask', r)
|
||||||
this.taskEditTask.startDate = (+ new Date(this.taskEditTask.startDate)) / 1000
|
message.success({message: 'The task was successfully updated.'}, this)
|
||||||
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
|
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
cancel()
|
message.error(e, this)
|
||||||
this.handleError(e)
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
addSubtask() {
|
addSubtask() {
|
||||||
this.newTask.parentTaskID = this.taskEditTask.id
|
this.newTask.parentTaskID = this.taskEditTask.id
|
||||||
this.addTask()
|
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) {
|
for (const t in this.list.tasks) {
|
||||||
if (this.list.tasks[t].id === id) {
|
if (this.list.tasks[t].id === task.id) {
|
||||||
this.$set(this.list.tasks, t, this.fixStuffComingFromAPI(updatedTask))
|
this.$set(this.list.tasks, t, task)
|
||||||
break
|
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) {
|
for (const s in this.list.tasks[t].subtasks) {
|
||||||
if (this.list.tasks[t].subtasks[s].id === updatedTask.id) {
|
if (this.list.tasks[t].subtasks[s].id === task.id) {
|
||||||
this.$set(this.list.tasks[t].subtasks, s, updatedTask)
|
this.$set(this.list.tasks[t].subtasks, s, task)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.list.tasks = this.sortTasks(this.list.tasks)
|
this.list.sortTasks()
|
||||||
},
|
|
||||||
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
|
|
||||||
},
|
},
|
||||||
updateLastReminderDate(selectedDates) {
|
updateLastReminderDate(selectedDates) {
|
||||||
this.lastReminder = +new Date(selectedDates[0])
|
this.lastReminder = +new Date(selectedDates[0])
|
||||||
|
@ -513,23 +366,6 @@
|
||||||
this.taskEditTask.reminderDates.splice(index, 1)
|
this.taskEditTask.reminderDates.splice(index, 1)
|
||||||
// Reset the last to 0 to have the "add reminder" button
|
// Reset the last to 0 to have the "add reminder" button
|
||||||
this.taskEditTask.reminderDates[this.taskEditTask.reminderDates.length - 1] = null
|
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 router from '../../router'
|
||||||
import {HTTP} from '../../http-common'
|
import {HTTP} from '../../http-common'
|
||||||
import message from '../../message'
|
import message from '../../message'
|
||||||
|
import TaskService from '../../services/task'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "ShowTasks",
|
name: "ShowTasks",
|
||||||
|
@ -39,7 +40,8 @@
|
||||||
return {
|
return {
|
||||||
loading: true,
|
loading: true,
|
||||||
tasks: [],
|
tasks: [],
|
||||||
hasUndoneTasks: false
|
hasUndoneTasks: false,
|
||||||
|
taskService: TaskService,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
@ -48,10 +50,22 @@
|
||||||
showAll: Boolean,
|
showAll: Boolean,
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
this.taskService = new TaskService()
|
||||||
this.loadPendingTasks()
|
this.loadPendingTasks()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
loadPendingTasks() {
|
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)
|
const cancel = message.setLoading(this)
|
||||||
|
|
||||||
let url = `tasks/all/duedate`
|
let url = `tasks/all/duedate`
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<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">
|
<div class="card">
|
||||||
<header class="card-header">
|
<header class="card-header">
|
||||||
<p class="card-header-title">
|
<p class="card-header-title">
|
||||||
|
@ -12,25 +12,25 @@
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="namespacetext">Namespace Name</label>
|
<label class="label" for="namespacetext">Namespace Name</label>
|
||||||
<div class="control">
|
<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>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="namespacedescription">Description</label>
|
<label class="label" for="namespacedescription">Description</label>
|
||||||
<div class="control">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="columns bigbuttons">
|
<div class="columns bigbuttons">
|
||||||
<div class="column">
|
<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
|
Save
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-1">
|
<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">
|
<span class="icon is-small">
|
||||||
<icon icon="trash-alt"/>
|
<icon icon="trash-alt"/>
|
||||||
</span>
|
</span>
|
||||||
|
@ -41,9 +41,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<manageusers :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>
|
||||||
<manageteams :id="namespace.id" type="namespace" :userIsAdmin="userIsAdmin" />
|
|
||||||
|
|
||||||
<modal
|
<modal
|
||||||
v-if="showDeleteModal"
|
v-if="showDeleteModal"
|
||||||
|
@ -59,21 +58,24 @@
|
||||||
<script>
|
<script>
|
||||||
import auth from '../../auth'
|
import auth from '../../auth'
|
||||||
import router from '../../router'
|
import router from '../../router'
|
||||||
import {HTTP} from '../../http-common'
|
|
||||||
import message from '../../message'
|
import message from '../../message'
|
||||||
import manageusers from '../sharing/user'
|
import manageusers from '../sharing/user'
|
||||||
import manageteams from '../sharing/team'
|
import manageteams from '../sharing/team'
|
||||||
|
import NamespaceService from '../../services/namespace'
|
||||||
|
import NamespaceModel from '../../models/namespace'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "EditNamespace",
|
name: "EditNamespace",
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
namespace: {title: '', description:''},
|
namespaceService: NamespaceService,
|
||||||
error: '',
|
userIsAdmin: false,
|
||||||
loading: false,
|
manageUsersComponent: '',
|
||||||
|
manageTeamsComponent: '',
|
||||||
|
|
||||||
|
namespace: NamespaceModel,
|
||||||
showDeleteModal: false,
|
showDeleteModal: false,
|
||||||
user: auth.user,
|
user: auth.user,
|
||||||
userIsAdmin: false,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
@ -89,6 +91,7 @@
|
||||||
this.namespace.id = this.$route.params.id
|
this.namespace.id = this.$route.params.id
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
this.namespaceService = new NamespaceService()
|
||||||
this.loadNamespace()
|
this.loadNamespace()
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -97,66 +100,47 @@
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
loadNamespace() {
|
loadNamespace() {
|
||||||
const cancel = message.setLoading(this)
|
let namespace = new NamespaceModel({id: this.$route.params.id})
|
||||||
|
this.namespaceService.get(namespace)
|
||||||
HTTP.get(`namespaces/` + this.$route.params.id, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
.then(r => {
|
||||||
.then(response => {
|
this.$set(this, 'namespace', r)
|
||||||
this.$set(this, 'namespace', response.data)
|
if (r.owner.id === this.user.infos.id) {
|
||||||
if (response.data.owner.id === this.user.infos.id) {
|
|
||||||
this.userIsAdmin = true
|
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 => {
|
.catch(e => {
|
||||||
cancel()
|
message.error(e, this)
|
||||||
this.handleError(e)
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
submit() {
|
submit() {
|
||||||
const cancel = message.setLoading(this)
|
this.namespaceService.update(this.namespace)
|
||||||
|
.then(r => {
|
||||||
HTTP.post(`namespaces/` + this.$route.params.id, this.namespace, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
|
||||||
.then(response => {
|
|
||||||
// Update the namespace in the parent
|
// Update the namespace in the parent
|
||||||
for (const n in this.$parent.namespaces) {
|
for (const n in this.$parent.namespaces) {
|
||||||
if (this.$parent.namespaces[n].id === response.data.id) {
|
if (this.$parent.namespaces[n].id === r.id) {
|
||||||
response.data.lists = this.$parent.namespaces[n].lists
|
r.lists = this.$parent.namespaces[n].lists
|
||||||
this.$set(this.$parent.namespaces, n, response.data)
|
this.$set(this.$parent.namespaces, n, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.handleSuccess({message: 'The namespace was successfully updated.'})
|
message.success({message: 'The namespace was successfully updated.'}, this)
|
||||||
cancel()
|
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
cancel()
|
message.error(e, this)
|
||||||
this.handleError(e)
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
deleteNamespace() {
|
deleteNamespace() {
|
||||||
const cancel = message.setLoading(this)
|
this.namespaceService.delete(this.namespace)
|
||||||
HTTP.delete(`namespaces/` + this.$route.params.id, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.handleSuccess({message: 'The namespace was successfully deleted.'})
|
message.success({message: 'The namespace was successfully deleted.'}, this)
|
||||||
cancel()
|
|
||||||
router.push({name: 'home'})
|
router.push({name: 'home'})
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
cancel()
|
|
||||||
this.handleError(e)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
handleError(e) {
|
|
||||||
message.error(e, this)
|
message.error(e, this)
|
||||||
},
|
})
|
||||||
handleSuccess(e) {
|
|
||||||
message.success(e, this)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.bigbuttons{
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -7,8 +7,8 @@
|
||||||
<h3>Create a new namespace</h3>
|
<h3>Create a new namespace</h3>
|
||||||
<form @submit.prevent="newNamespace" @keyup.esc="back()">
|
<form @submit.prevent="newNamespace" @keyup.esc="back()">
|
||||||
<div class="field is-grouped">
|
<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': namespaceService.loading}">
|
||||||
<input v-focus class="input" v-bind:class="{ 'disabled': loading}" v-model="namespace.name" type="text" placeholder="The namespace's name goes here...">
|
<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>
|
||||||
<p class="control">
|
<p class="control">
|
||||||
<button type="submit" class="button is-success noshadow">
|
<button type="submit" class="button is-success noshadow">
|
||||||
|
@ -27,16 +27,16 @@
|
||||||
<script>
|
<script>
|
||||||
import auth from '../../auth'
|
import auth from '../../auth'
|
||||||
import router from '../../router'
|
import router from '../../router'
|
||||||
import {HTTP} from '../../http-common'
|
|
||||||
import message from '../../message'
|
import message from '../../message'
|
||||||
|
import NamespaceModel from "../../models/namespace";
|
||||||
|
import NamespaceService from "../../services/namespace";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "NewNamespace",
|
name: "NewNamespace",
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
namespace: {title: ''},
|
namespace: NamespaceModel,
|
||||||
error: '',
|
namespaceService: NamespaceService,
|
||||||
loading: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeMount() {
|
beforeMount() {
|
||||||
|
@ -46,32 +46,24 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
this.namespace = new NamespaceModel()
|
||||||
|
this.namespaceService = new NamespaceService()
|
||||||
this.$parent.setFullPage();
|
this.$parent.setFullPage();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
newNamespace() {
|
newNamespace() {
|
||||||
const cancel = message.setLoading(this)
|
this.namespaceService.create(this.namespace)
|
||||||
|
|
||||||
HTTP.put(`namespaces`, this.namespace, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$parent.loadNamespaces()
|
this.$parent.loadNamespaces()
|
||||||
this.handleSuccess({message: 'The namespace was successfully created.'})
|
message.success({message: 'The namespace was successfully created.'}, this)
|
||||||
cancel()
|
|
||||||
router.push({name: 'home'})
|
router.push({name: 'home'})
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
cancel()
|
message.error(e, this)
|
||||||
this.handleError(e)
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
back() {
|
back() {
|
||||||
router.go(-1)
|
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">
|
<div class="card-content content teams-list">
|
||||||
<form @submit.prevent="addTeam()" class="add-team-form" v-if="userIsAdmin">
|
<form @submit.prevent="addTeam()" class="add-team-form" v-if="userIsAdmin">
|
||||||
<div class="field is-grouped">
|
<div class="field is-grouped">
|
||||||
<p class="control has-icons-left is-expanded" v-bind:class="{ 'is-loading': loading}">
|
<p class="control has-icons-left is-expanded" v-bind:class="{ 'is-loading': this.teamService.loading}">
|
||||||
<input class="input" v-bind:class="{ 'disabled': loading}" v-model.number="newTeam.team_id" type="text" placeholder="Add a new team...">
|
<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">
|
<span class="icon is-small is-left">
|
||||||
<icon icon="users"/>
|
<icon icon="users"/>
|
||||||
</span>
|
</span>
|
||||||
|
@ -86,9 +86,12 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {HTTP} from '../../http-common'
|
|
||||||
import auth from '../../auth'
|
import auth from '../../auth'
|
||||||
import message from '../../message'
|
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 {
|
export default {
|
||||||
name: 'team',
|
name: 'team',
|
||||||
|
@ -99,11 +102,13 @@
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
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,
|
currentUser: auth.user.infos,
|
||||||
typeString: '',
|
typeString: '',
|
||||||
listTeams: [],
|
listTeams: [],
|
||||||
newTeam: {team_id: 0},
|
newTeam: {teamID: 0},
|
||||||
showTeamDeleteModal: false,
|
showTeamDeleteModal: false,
|
||||||
teamToDelete: 0,
|
teamToDelete: 0,
|
||||||
}
|
}
|
||||||
|
@ -111,8 +116,12 @@
|
||||||
created() {
|
created() {
|
||||||
if (this.type === 'list') {
|
if (this.type === 'list') {
|
||||||
this.typeString = `list`
|
this.typeString = `list`
|
||||||
|
this.teamService = new TeamListService()
|
||||||
|
this.teamStuffModel = new TeamListModel({listID: this.id})
|
||||||
} else if (this.type === 'namespace') {
|
} else if (this.type === 'namespace') {
|
||||||
this.typeString = `namespace`
|
this.typeString = `namespace`
|
||||||
|
this.teamService = new TeamNamespaceService()
|
||||||
|
this.teamStuffModel = new TeamNamespaceModel({namespaceID: this.id})
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Unknown type: ' + this.type)
|
throw new Error('Unknown type: ' + this.type)
|
||||||
}
|
}
|
||||||
|
@ -121,75 +130,61 @@
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
loadTeams() {
|
loadTeams() {
|
||||||
const cancel = message.setLoading(this)
|
this.teamService.getAll(this.teamStuffModel)
|
||||||
HTTP.get(this.typeString + `s/` + this.id + `/teams`, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
.then(r => {
|
||||||
.then(response => {
|
this.$set(this, 'listTeams', r)
|
||||||
this.$set(this, 'listTeams', response.data)
|
|
||||||
cancel()
|
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
cancel()
|
message.error(e, this)
|
||||||
this.handleError(e)
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
deleteTeam() {
|
deleteTeam() {
|
||||||
const cancel = message.setLoading(this)
|
this.teamService.delete(this.teamStuffModel)
|
||||||
HTTP.delete(this.typeString + `s/` + this.id + `/teams/` + this.teamToDelete, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.showTeamDeleteModal = false;
|
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()
|
this.loadTeams()
|
||||||
cancel()
|
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
cancel()
|
message.error(e, this)
|
||||||
this.handleError(e)
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
addTeam(admin) {
|
addTeam(admin) {
|
||||||
const cancel = message.setLoading(this)
|
|
||||||
if(admin === null) {
|
if(admin === null) {
|
||||||
admin = false
|
admin = false
|
||||||
}
|
}
|
||||||
this.newTeam.right = 0
|
this.teamStuffModel.right = 0
|
||||||
if (admin) {
|
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(() => {
|
.then(() => {
|
||||||
|
// FIXME: this should add the team to the list instead of loading it again
|
||||||
this.loadTeams()
|
this.loadTeams()
|
||||||
this.handleSuccess({message: 'The team was successfully added.'})
|
message.success({message: 'The team was successfully added.'}, this)
|
||||||
cancel()
|
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
cancel()
|
message.error(e, this)
|
||||||
this.handleError(e)
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
toggleTeamType(teamid, current) {
|
toggleTeamType(teamid, current) {
|
||||||
const cancel = message.setLoading(this)
|
this.teamStuffModel.teamID = teamid
|
||||||
let right = 0
|
this.teamStuffModel.right = 0
|
||||||
if (!current) {
|
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(() => {
|
.then(() => {
|
||||||
|
// FIXME: this should update the team in the list instead of loading it again
|
||||||
this.loadTeams()
|
this.loadTeams()
|
||||||
this.handleSuccess({message: 'The team right was successfully updated.'})
|
message.success({message: 'The team right was successfully updated.'}, this)
|
||||||
cancel()
|
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
cancel()
|
|
||||||
this.handleError(e)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
handleError(e) {
|
|
||||||
message.error(e, this)
|
message.error(e, this)
|
||||||
},
|
})
|
||||||
handleSuccess(e) {
|
|
||||||
message.success(e, this)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,21 +9,20 @@
|
||||||
<div class="card-content content users-list">
|
<div class="card-content content users-list">
|
||||||
<form @submit.prevent="addUser()" class="add-user-form" v-if="userIsAdmin">
|
<form @submit.prevent="addUser()" class="add-user-form" v-if="userIsAdmin">
|
||||||
<div class="field is-grouped">
|
<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
|
<multiselect
|
||||||
v-model="newUser"
|
v-model="user"
|
||||||
:options="foundUsers"
|
:options="foundUsers"
|
||||||
:multiple="false"
|
:multiple="false"
|
||||||
:searchable="true"
|
:searchable="true"
|
||||||
:loading="loading"
|
:loading="userService.loading"
|
||||||
:internal-search="true"
|
:internal-search="true"
|
||||||
@search-change="findUsers"
|
@search-change="findUsers"
|
||||||
placeholder="Type to search a user"
|
placeholder="Type to search a user"
|
||||||
label="username"
|
label="username"
|
||||||
track-by="user_id">
|
track-by="id">
|
||||||
|
|
||||||
<template slot="clear" slot-scope="props">
|
<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>
|
</template>
|
||||||
<span slot="noResult">Oops! No users found. Consider changing the search query.</span>
|
<span slot="noResult">Oops! No users found. Consider changing the search query.</span>
|
||||||
</multiselect>
|
</multiselect>
|
||||||
|
@ -77,7 +76,7 @@
|
||||||
Admin
|
Admin
|
||||||
</template>
|
</template>
|
||||||
</button>
|
</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">
|
<span class="icon is-small">
|
||||||
<icon icon="trash-alt"/>
|
<icon icon="trash-alt"/>
|
||||||
</span>
|
</span>
|
||||||
|
@ -100,12 +99,18 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {HTTP} from '../../http-common'
|
|
||||||
import auth from '../../auth'
|
import auth from '../../auth'
|
||||||
import message from '../../message'
|
import message from '../../message'
|
||||||
import multiselect from 'vue-multiselect'
|
import multiselect from 'vue-multiselect'
|
||||||
import 'vue-multiselect/dist/vue-multiselect.min.css'
|
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 {
|
export default {
|
||||||
name: 'user',
|
name: 'user',
|
||||||
props: {
|
props: {
|
||||||
|
@ -115,14 +120,15 @@
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
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,
|
currentUser: auth.user.infos,
|
||||||
typeString: '',
|
typeString: '',
|
||||||
showUserDeleteModal: false,
|
showUserDeleteModal: false,
|
||||||
users: [],
|
users: [],
|
||||||
newUser: {username: '', user_id: 0},
|
|
||||||
userToDelete: 0,
|
|
||||||
newUserid: 0,
|
|
||||||
foundUsers: [],
|
foundUsers: [],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -130,10 +136,17 @@
|
||||||
multiselect
|
multiselect
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
this.userService = new UserService()
|
||||||
|
this.user = new UserModel()
|
||||||
|
|
||||||
if (this.type === 'list') {
|
if (this.type === 'list') {
|
||||||
this.typeString = `list`
|
this.typeString = `list`
|
||||||
|
this.userStuffService = new UserListService()
|
||||||
|
this.userStuff = new UserListModel({listID: this.id})
|
||||||
} else if (this.type === 'namespace') {
|
} else if (this.type === 'namespace') {
|
||||||
this.typeString = `namespace`
|
this.typeString = `namespace`
|
||||||
|
this.userStuffService = new UserNamespaceService()
|
||||||
|
this.userStuff = new UserNamespaceModel({namespaceID: this.id})
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Unknown type: ' + this.type)
|
throw new Error('Unknown type: ' + this.type)
|
||||||
}
|
}
|
||||||
|
@ -142,100 +155,76 @@
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
loadUsers() {
|
loadUsers() {
|
||||||
const cancel = message.setLoading(this)
|
this.userStuffService.getAll(this.userStuff)
|
||||||
HTTP.get(this.typeString + `s/` + this.id + `/users`, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
|
||||||
.then(response => {
|
.then(response => {
|
||||||
//response.data.push(this.list.owner)
|
this.$set(this, 'users', response)
|
||||||
this.$set(this, 'users', response.data)
|
|
||||||
cancel()
|
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
cancel()
|
message.error(e, this)
|
||||||
this.handleError(e)
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
deleteUser() {
|
deleteUser() {
|
||||||
const cancel = message.setLoading(this)
|
// The api wants the user id as userID
|
||||||
HTTP.delete(this.typeString + `s/` + this.id + `/users/` + this.userToDelete, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
let usr = this.user
|
||||||
|
this.userStuff.userID = usr.id
|
||||||
|
|
||||||
|
this.userStuffService.delete(this.userStuff)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.showUserDeleteModal = false;
|
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()
|
this.loadUsers()
|
||||||
cancel()
|
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
cancel()
|
message.error(e, this)
|
||||||
this.handleError(e)
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
addUser(admin) {
|
addUser(admin = false) {
|
||||||
const cancel = message.setLoading(this)
|
this.userStuff.right = 0
|
||||||
if(admin === null) {
|
|
||||||
admin = false
|
|
||||||
}
|
|
||||||
this.newUser.right = 0
|
|
||||||
if (admin) {
|
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', [])
|
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(() => {
|
.then(() => {
|
||||||
this.loadUsers()
|
this.loadUsers()
|
||||||
this.newUser = {}
|
message.success({message: 'The user was successfully added.'}, this)
|
||||||
this.handleSuccess({message: 'The user was successfully added.'})
|
|
||||||
cancel()
|
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
cancel()
|
message.error(e, this)
|
||||||
this.handleError(e)
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
toggleUserType(userid, current) {
|
toggleUserType(userid, current) {
|
||||||
const cancel = message.setLoading(this)
|
this.userStuff.userID = userid
|
||||||
let right = 0
|
this.userStuff.right = 0
|
||||||
if (!current) {
|
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(() => {
|
.then(() => {
|
||||||
this.loadUsers()
|
this.loadUsers()
|
||||||
this.handleSuccess({message: 'The user right was successfully updated.'})
|
message.success({message: 'The user right was successfully updated.'}, this)
|
||||||
cancel()
|
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
cancel()
|
message.error(e, this)
|
||||||
this.handleError(e)
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
findUsers(query) {
|
findUsers(query) {
|
||||||
const cancel = message.setLoading(this)
|
|
||||||
if(query === '') {
|
if(query === '') {
|
||||||
this.$set(this, 'foundUsers', [])
|
this.$set(this, 'foundUsers', [])
|
||||||
cancel()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$set(this, 'newUser', {username: '', user_id: 0})
|
this.userService.getAll({}, {s: query})
|
||||||
|
|
||||||
HTTP.get(`users?s=` + query, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
|
||||||
.then(response => {
|
.then(response => {
|
||||||
this.$set(this, 'foundUsers', [])
|
this.$set(this, 'foundUsers', response)
|
||||||
|
|
||||||
for (const u in response.data) {
|
|
||||||
this.foundUsers.push({
|
|
||||||
username: response.data[u].username,
|
|
||||||
user_id: response.data[u].id,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel()
|
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
cancel()
|
message.error(e, this)
|
||||||
this.handleError(e)
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
clearAll () {
|
clearAll () {
|
||||||
|
@ -244,12 +233,6 @@
|
||||||
limitText (count) {
|
limitText (count) {
|
||||||
return `and ${count} others`
|
return `and ${count} others`
|
||||||
},
|
},
|
||||||
handleError(e) {
|
|
||||||
message.error(e, this)
|
|
||||||
},
|
|
||||||
handleSuccess(e) {
|
|
||||||
message.success(e, this)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<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">
|
<div class="card" v-if="userIsAdmin">
|
||||||
<header class="card-header">
|
<header class="card-header">
|
||||||
<p class="card-header-title">
|
<p class="card-header-title">
|
||||||
|
@ -12,25 +12,25 @@
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="teamtext">Team Name</label>
|
<label class="label" for="teamtext">Team Name</label>
|
||||||
<div class="control">
|
<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>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="teamdescription">Description</label>
|
<label class="label" for="teamdescription">Description</label>
|
||||||
<div class="control">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="columns bigbuttons">
|
<div class="columns bigbuttons">
|
||||||
<div class="column">
|
<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
|
Save
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-1">
|
<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">
|
<span class="icon is-small">
|
||||||
<icon icon="trash-alt"/>
|
<icon icon="trash-alt"/>
|
||||||
</span>
|
</span>
|
||||||
|
@ -50,8 +50,8 @@
|
||||||
<div class="card-content content team-members">
|
<div class="card-content content team-members">
|
||||||
<form @submit.prevent="addUser()" class="add-member-form" v-if="userIsAdmin">
|
<form @submit.prevent="addUser()" class="add-member-form" v-if="userIsAdmin">
|
||||||
<div class="field is-grouped">
|
<div class="field is-grouped">
|
||||||
<p class="control has-icons-left is-expanded" v-bind:class="{ 'is-loading': loading}">
|
<p class="control has-icons-left is-expanded" v-bind:class="{ 'is-loading': teamMemberService.loading}">
|
||||||
<input class="input" v-bind:class="{ 'disabled': loading}" v-model.number="newUser.id" type="text" placeholder="Add a new user...">
|
<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">
|
<span class="icon is-small is-left">
|
||||||
<icon icon="user"/>
|
<icon icon="user"/>
|
||||||
</span>
|
</span>
|
||||||
|
@ -90,7 +90,7 @@
|
||||||
</template>
|
</template>
|
||||||
</td>
|
</td>
|
||||||
<td class="actions" v-if="userIsAdmin">
|
<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
|
Make
|
||||||
<template v-if="!m.admin">
|
<template v-if="!m.admin">
|
||||||
Admin
|
Admin
|
||||||
|
@ -99,7 +99,7 @@
|
||||||
Member
|
Member
|
||||||
</template>
|
</template>
|
||||||
</button>
|
</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">
|
<span class="icon is-small">
|
||||||
<icon icon="trash-alt"/>
|
<icon icon="trash-alt"/>
|
||||||
</span>
|
</span>
|
||||||
|
@ -137,22 +137,26 @@
|
||||||
<script>
|
<script>
|
||||||
import auth from '../../auth'
|
import auth from '../../auth'
|
||||||
import router from '../../router'
|
import router from '../../router'
|
||||||
import {HTTP} from '../../http-common'
|
|
||||||
import message from '../../message'
|
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 {
|
export default {
|
||||||
name: "EditTeam",
|
name: "EditTeam",
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
team: {title: '', description:''},
|
teamService: TeamService,
|
||||||
error: '',
|
teamMemberService: TeamMemberService,
|
||||||
loading: false,
|
team: TeamModel,
|
||||||
|
member: TeamMemberModel,
|
||||||
|
|
||||||
showDeleteModal: false,
|
showDeleteModal: false,
|
||||||
showUserDeleteModal: false,
|
showUserDeleteModal: false,
|
||||||
user: auth.user,
|
user: auth.user,
|
||||||
userIsAdmin: false,
|
userIsAdmin: false,
|
||||||
userToDelete: 0,
|
|
||||||
newUser: {id:0},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeMount() {
|
beforeMount() {
|
||||||
|
@ -162,6 +166,8 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
this.teamService = new TeamService()
|
||||||
|
this.teamMemberService = new TeamMemberService()
|
||||||
this.loadTeam()
|
this.loadTeam()
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -170,97 +176,71 @@
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
loadTeam() {
|
loadTeam() {
|
||||||
const cancel = message.setLoading(this)
|
this.member = new TeamMemberModel({teamID: this.$route.params.id})
|
||||||
|
this.team = new TeamModel({id: this.$route.params.id})
|
||||||
HTTP.get(`teams/` + this.$route.params.id, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
this.teamService.get(this.team)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
this.$set(this, 'team', response.data)
|
this.$set(this, 'team', response)
|
||||||
let members = response.data.members
|
let members = response.members
|
||||||
for (const m in members) {
|
for (const m in members) {
|
||||||
|
members[m].teamID = this.$route.params.id
|
||||||
if (members[m].id === this.user.infos.id && members[m].admin) {
|
if (members[m].id === this.user.infos.id && members[m].admin) {
|
||||||
this.userIsAdmin = true
|
this.userIsAdmin = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cancel()
|
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
this.handleError(e)
|
message.error(e, this)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
submit() {
|
submit() {
|
||||||
const cancel = message.setLoading(this)
|
this.teamService.update(this.team)
|
||||||
|
|
||||||
HTTP.post(`teams/` + this.$route.params.id, this.team, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
|
||||||
.then(response => {
|
.then(response => {
|
||||||
// Update the team in the parent
|
this.team = response
|
||||||
for (const n in this.$parent.teams) {
|
message.success({message: 'The team was successfully updated.'}, this)
|
||||||
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()
|
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
cancel()
|
message.error(e, this)
|
||||||
this.handleError(e)
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
deleteTeam() {
|
deleteTeam() {
|
||||||
const cancel = message.setLoading(this)
|
this.teamService.delete(this.team)
|
||||||
HTTP.delete(`teams/` + this.$route.params.id, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.handleSuccess({message: 'The team was successfully deleted.'})
|
message.success({message: 'The team was successfully deleted.'}, this)
|
||||||
cancel()
|
router.push({name: 'listTeams'})
|
||||||
router.push({name: 'home'})
|
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
cancel()
|
message.error(e, this)
|
||||||
this.handleError(e)
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
deleteUser() {
|
deleteUser() {
|
||||||
const cancel = message.setLoading(this)
|
this.teamMemberService.delete(this.member)
|
||||||
HTTP.delete(`teams/` + this.$route.params.id + `/members/` + this.userToDelete, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.showUserDeleteModal = false;
|
message.success({message: 'The user was successfully deleted from the team.'}, this)
|
||||||
this.handleSuccess({message: 'The user was successfully deleted from the team.'})
|
|
||||||
this.loadTeam()
|
this.loadTeam()
|
||||||
cancel()
|
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.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)
|
message.error(e, this)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.showUserDeleteModal = false
|
||||||
|
})
|
||||||
},
|
},
|
||||||
handleSuccess(e) {
|
addUser() {
|
||||||
message.success(e, this)
|
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>
|
<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" >
|
<router-link :to="{name:'newTeam'}" class="button is-success button-right" >
|
||||||
<span class="icon is-small">
|
<span class="icon is-small">
|
||||||
<icon icon="plus"/>
|
<icon icon="plus"/>
|
||||||
|
@ -20,16 +20,15 @@
|
||||||
<script>
|
<script>
|
||||||
import auth from '../../auth'
|
import auth from '../../auth'
|
||||||
import router from '../../router'
|
import router from '../../router'
|
||||||
import {HTTP} from '../../http-common'
|
|
||||||
import message from '../../message'
|
import message from '../../message'
|
||||||
|
import TeamService from '../../services/team'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "ListTeams",
|
name: "ListTeams",
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
teamService: TeamService,
|
||||||
teams: [],
|
teams: [],
|
||||||
error: '',
|
|
||||||
loading: false,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeMount() {
|
beforeMount() {
|
||||||
|
@ -39,23 +38,18 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
this.teamService = new TeamService()
|
||||||
this.loadTeams()
|
this.loadTeams()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
loadTeams() {
|
loadTeams() {
|
||||||
const cancel = message.setLoading(this)
|
this.teamService.getAll()
|
||||||
|
|
||||||
HTTP.get(`teams`, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
|
||||||
.then(response => {
|
.then(response => {
|
||||||
this.$set(this, 'teams', response.data)
|
this.$set(this, 'teams', response)
|
||||||
cancel()
|
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
this.handleError(e)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
handleError(e) {
|
|
||||||
message.error(e, this)
|
message.error(e, this)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,8 +7,8 @@
|
||||||
<h3>Create a new team</h3>
|
<h3>Create a new team</h3>
|
||||||
<form @submit.prevent="newTeam" @keyup.esc="back()">
|
<form @submit.prevent="newTeam" @keyup.esc="back()">
|
||||||
<div class="field is-grouped">
|
<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': teamService.loading}">
|
||||||
<input v-focus class="input" v-bind:class="{ 'disabled': loading}" v-model="team.name" type="text" placeholder="The team's name goes here...">
|
<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>
|
||||||
<p class="control">
|
<p class="control">
|
||||||
<button type="submit" class="button is-success noshadow">
|
<button type="submit" class="button is-success noshadow">
|
||||||
|
@ -26,16 +26,16 @@
|
||||||
<script>
|
<script>
|
||||||
import auth from '../../auth'
|
import auth from '../../auth'
|
||||||
import router from '../../router'
|
import router from '../../router'
|
||||||
import {HTTP} from '../../http-common'
|
|
||||||
import message from '../../message'
|
import message from '../../message'
|
||||||
|
import TeamModel from '../../models/team'
|
||||||
|
import TeamService from '../../services/team'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "NewTeam",
|
name: "NewTeam",
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
team: {title: ''},
|
teamService: TeamService,
|
||||||
error: '',
|
team: TeamModel,
|
||||||
loading: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeMount() {
|
beforeMount() {
|
||||||
|
@ -45,32 +45,23 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
this.teamService = new TeamService()
|
||||||
this.$parent.setFullPage();
|
this.$parent.setFullPage();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
newTeam() {
|
newTeam() {
|
||||||
const cancel = message.setLoading(this)
|
this.teamService.create(this.team)
|
||||||
|
|
||||||
HTTP.put(`teams`, this.team, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
|
||||||
.then(response => {
|
.then(response => {
|
||||||
router.push({name:'editTeam', params:{id: response.data.id}})
|
router.push({name:'editTeam', params:{id: response.id}})
|
||||||
this.handleSuccess({message: 'The team was successfully created.'})
|
message.success({message: 'The team was successfully created.'}, this)
|
||||||
cancel()
|
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
cancel()
|
message.error(e, this)
|
||||||
this.handleError(e)
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
back() {
|
back() {
|
||||||
router.go(-1)
|
router.go(-1)
|
||||||
},
|
},
|
||||||
handleError(e) {
|
|
||||||
message.error(e, this)
|
|
||||||
},
|
|
||||||
handleSuccess(e) {
|
|
||||||
message.success(e, this)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -52,6 +52,7 @@
|
||||||
},
|
},
|
||||||
beforeMount() {
|
beforeMount() {
|
||||||
// Try to verify the email
|
// Try to verify the email
|
||||||
|
// FIXME: Why is this here? Can we find a better place for this?
|
||||||
let emailVerifyToken = localStorage.getItem('emailConfirmToken')
|
let emailVerifyToken = localStorage.getItem('emailConfirmToken')
|
||||||
if (emailVerifyToken) {
|
if (emailVerifyToken) {
|
||||||
const cancel = message.setLoading(this)
|
const cancel = message.setLoading(this)
|
||||||
|
|
|
@ -16,10 +16,10 @@
|
||||||
|
|
||||||
<div class="field is-grouped">
|
<div class="field is-grouped">
|
||||||
<div class="control">
|
<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>
|
</div>
|
||||||
<div class="notification is-info" v-if="loading">
|
<div class="notification is-info" v-if="this.passwordResetService.loading">
|
||||||
Loading...
|
Loading...
|
||||||
</div>
|
</div>
|
||||||
<div class="notification is-danger" v-if="error">
|
<div class="notification is-danger" v-if="error">
|
||||||
|
@ -37,53 +37,42 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {HTTP} from '../../http-common'
|
import PasswordResetModel from '../../models/passwordReset'
|
||||||
import message from '../../message'
|
import PasswordResetService from '../../services/passwordReset'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
passwordResetService: PasswordResetService,
|
||||||
credentials: {
|
credentials: {
|
||||||
password: '',
|
password: '',
|
||||||
password2: '',
|
password2: '',
|
||||||
},
|
},
|
||||||
error: '',
|
error: '',
|
||||||
successMessage: '',
|
successMessage: ''
|
||||||
loading: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
created() {
|
||||||
|
this.passwordResetService = new PasswordResetService()
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
submit() {
|
submit() {
|
||||||
const cancel = message.setLoading(this)
|
|
||||||
this.error = ''
|
this.error = ''
|
||||||
|
|
||||||
if (this.credentials.password2 !== this.credentials.password) {
|
if (this.credentials.password2 !== this.credentials.password) {
|
||||||
cancel()
|
|
||||||
this.error = 'Passwords don\'t match'
|
this.error = 'Passwords don\'t match'
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let resetPasswordPayload = {
|
let passwordReset = new PasswordResetModel({new_password: this.credentials.password})
|
||||||
token: localStorage.getItem('passwordResetToken'),
|
this.passwordResetService.resetPassword(passwordReset)
|
||||||
new_password: this.credentials.password
|
|
||||||
}
|
|
||||||
|
|
||||||
HTTP.post(`user/password/reset`, resetPasswordPayload)
|
|
||||||
.then(response => {
|
.then(response => {
|
||||||
this.handleSuccess(response)
|
this.successMessage = response.data.message
|
||||||
localStorage.removeItem('passwordResetToken')
|
localStorage.removeItem('passwordResetToken')
|
||||||
cancel()
|
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
this.error = e.response.data.message
|
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">
|
<form id="loginform" @submit.prevent="submit" v-if="!isSuccess">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="control">
|
<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>
|
</div>
|
||||||
|
|
||||||
<div class="field is-grouped">
|
<div class="field is-grouped">
|
||||||
<div class="control">
|
<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>
|
<router-link :to="{ name: 'login' }" class="button">Login</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -30,38 +30,32 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {HTTP} from '../../http-common'
|
import PasswordResetModel from '../../models/passwordReset'
|
||||||
import message from '../../message'
|
import PasswordResetService from '../../services/passwordReset'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
email: '',
|
passwordResetService: PasswordResetService,
|
||||||
|
passwordReset: PasswordResetModel,
|
||||||
error: '',
|
error: '',
|
||||||
isSuccess: false,
|
isSuccess: false
|
||||||
loading: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
created() {
|
||||||
|
this.passwordResetService = new PasswordResetService()
|
||||||
|
this.passwordReset = new PasswordResetModel()
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
submit() {
|
submit() {
|
||||||
const cancel = message.setLoading(this)
|
|
||||||
this.error = ''
|
this.error = ''
|
||||||
let credentials = {
|
this.passwordResetService.requestResetPassword(this.passwordReset)
|
||||||
email: this.email,
|
|
||||||
}
|
|
||||||
|
|
||||||
HTTP.post(`user/password/token`, credentials)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
cancel()
|
|
||||||
this.isSuccess = true
|
this.isSuccess = true
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
cancel()
|
|
||||||
this.handleError(e)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
handleError(e) {
|
|
||||||
this.error = e.response.data.message
|
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] Einzelne Teams ansehbar
|
||||||
* [x] In den Teams, in denen der Nutzer admin ist, Bearbeitung ermöglichen
|
* [x] In den Teams, in denen der Nutzer admin ist, Bearbeitung ermöglichen
|
||||||
* [x] Löschen ermöglichen
|
* [x] Löschen ermöglichen
|
||||||
|
* [x] Subtasks
|
||||||
|
* [x] Start/Enddatum für Tasks
|
||||||
|
* [x] Tasks in time range
|
||||||
* [ ] Search everything
|
* [ ] Search everything
|
||||||
* [ ] Lists
|
* [ ] Lists
|
||||||
* [ ] Tasks
|
* [ ] Tasks
|
||||||
|
@ -86,21 +89,31 @@
|
||||||
* [ ] Users with access to a namespace
|
* [ ] Users with access to a namespace
|
||||||
* [ ] Teams with access to a list
|
* [ ] Teams with access to a list
|
||||||
* [ ] Teams with access to a namespace
|
* [ ] Teams with access to a namespace
|
||||||
* [x] Subtasks
|
|
||||||
* [x] Start/Enddatum für Tasks
|
|
||||||
* [x] Tasks in time range
|
|
||||||
* [ ] Priorities
|
* [ ] Priorities
|
||||||
* [ ] Sachen mit hoher Prio irgendwie hervorheben (rotes Dreieck zb)
|
* [ ] Sachen mit hoher Prio irgendwie hervorheben (rotes Dreieck zb)
|
||||||
* [ ] Listen Kopieren
|
* [ ] Assignees
|
||||||
* [ ] "Move to Vikunja" -> Migrator von Wunderlist/todoist/etc
|
* [ ] Labels
|
||||||
* [ ] Timeline/Calendar view -> Dazu tasks die in einem Bestimmten Bereich due sind, macht dann das Frontend
|
* [ ] 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
|
## Waiting for backend
|
||||||
|
|
||||||
* [ ] Assignees
|
|
||||||
* [ ] In und Out webhooks, mit Templates vom Payload
|
* [ ] In und Out webhooks, mit Templates vom Payload
|
||||||
* [ ] "Smart Lists", Listen nach bestimmten Kriterien gefiltert -> nur UI?
|
* [ ] "Smart Lists", Listen nach bestimmten Kriterien gefiltert -> nur UI?
|
||||||
* [ ] "Performance-Statistik" -> Wie viele Tasks man in bestimmten Zeiträumen so geschafft hat etc
|
* [ ] "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"
|
* [ ] Activity Feed, so à la "der und der hat das und das gemacht etc"
|
||||||
* [ ] Attachments for tasks
|
* [ ] Attachments for tasks
|
||||||
* [ ] Labels
|
|
38
yarn.lock
38
yarn.lock
|
@ -2710,7 +2710,7 @@ debug@^4.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
ms "^2.1.1"
|
ms "^2.1.1"
|
||||||
|
|
||||||
debuglog@*, debuglog@^1.0.1:
|
debuglog@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492"
|
resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492"
|
||||||
integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=
|
integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=
|
||||||
|
@ -4365,7 +4365,7 @@ import-local@^2.0.0:
|
||||||
pkg-dir "^3.0.0"
|
pkg-dir "^3.0.0"
|
||||||
resolve-cwd "^2.0.0"
|
resolve-cwd "^2.0.0"
|
||||||
|
|
||||||
imurmurhash@*, imurmurhash@^0.1.4:
|
imurmurhash@^0.1.4:
|
||||||
version "0.1.4"
|
version "0.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
|
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
|
||||||
integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
|
integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
|
||||||
|
@ -5165,11 +5165,6 @@ lockfile@^1.0.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
signal-exit "^3.0.2"
|
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:
|
lodash._baseuniq@~4.6.0:
|
||||||
version "4.6.0"
|
version "4.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8"
|
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._createset "~4.0.0"
|
||||||
lodash._root "~3.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:
|
lodash._createset@~4.0.0:
|
||||||
version "4.0.3"
|
version "4.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26"
|
resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26"
|
||||||
integrity sha1-D0ZZ+7CddRlPqeK4imZE02PJ/iY=
|
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:
|
lodash._root@~3.0.0:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/lodash._root/-/lodash._root-3.0.1.tgz#fba1c4524c19ee9a5f8136b4609f017cf4ded692"
|
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"
|
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927"
|
||||||
integrity sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ==
|
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:
|
lodash.tail@^4.1.1:
|
||||||
version "4.1.1"
|
version "4.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664"
|
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"
|
isarray "0.0.1"
|
||||||
string_decoder "~0.10.x"
|
string_decoder "~0.10.x"
|
||||||
|
|
||||||
readdir-scoped-modules@*, readdir-scoped-modules@^1.0.0:
|
readdir-scoped-modules@^1.0.0:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.0.2.tgz#9fafa37d286be5d92cbaebdee030dc9b5f406747"
|
resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.0.2.tgz#9fafa37d286be5d92cbaebdee030dc9b5f406747"
|
||||||
integrity sha1-n6+jfShr5dksuuve4DDcm19AZ0c=
|
integrity sha1-n6+jfShr5dksuuve4DDcm19AZ0c=
|
||||||
|
|
Loading…
Reference in a new issue