chore: move frontend files
This commit is contained in:
462
frontend/src/services/abstractService.ts
Normal file
462
frontend/src/services/abstractService.ts
Normal file
@ -0,0 +1,462 @@
|
||||
import {AuthenticatedHTTPFactory} from '@/helpers/fetcher'
|
||||
import type {Method} from 'axios'
|
||||
|
||||
import {objectToSnakeCase} from '@/helpers/case'
|
||||
import AbstractModel from '@/models/abstractModel'
|
||||
import type {IAbstract} from '@/modelTypes/IAbstract'
|
||||
import type {Right} from '@/constants/rights'
|
||||
|
||||
interface Paths {
|
||||
create : string
|
||||
get : string
|
||||
getAll : string
|
||||
update : string
|
||||
delete : string
|
||||
reset?: string
|
||||
}
|
||||
|
||||
function convertObject(o: Record<string, unknown>) {
|
||||
if (o instanceof Date) {
|
||||
return o.toISOString()
|
||||
}
|
||||
|
||||
return o
|
||||
}
|
||||
|
||||
function prepareParams(params: Record<string, unknown | unknown[]>) {
|
||||
if (typeof params !== 'object') {
|
||||
return params
|
||||
}
|
||||
|
||||
for (const p in params) {
|
||||
if (Array.isArray(params[p])) {
|
||||
params[p] = params[p].map(convertObject)
|
||||
continue
|
||||
}
|
||||
|
||||
params[p] = convertObject(params[p])
|
||||
}
|
||||
|
||||
return objectToSnakeCase(params)
|
||||
}
|
||||
|
||||
export default abstract class AbstractService<Model extends IAbstract = IAbstract> {
|
||||
|
||||
/////////////////////////////
|
||||
// Initial variable definitions
|
||||
///////////////////////////
|
||||
|
||||
http
|
||||
loading = false
|
||||
uploadProgress = 0
|
||||
paths: Paths = {
|
||||
create: '',
|
||||
get: '',
|
||||
getAll: '',
|
||||
update: '',
|
||||
delete: '',
|
||||
}
|
||||
// This contains the total number of pages and the number of results for the current page
|
||||
totalPages = 0
|
||||
resultCount = 0
|
||||
|
||||
/////////////
|
||||
// Service init
|
||||
///////////
|
||||
|
||||
/**
|
||||
* The abstract constructor.
|
||||
* @param [paths] An object with all paths.
|
||||
*/
|
||||
constructor(paths : Partial<Paths> = {}) {
|
||||
this.http = AuthenticatedHTTPFactory()
|
||||
|
||||
// Set the interceptors to process every request
|
||||
this.http.interceptors.request.use((config) => {
|
||||
switch (config.method) {
|
||||
case 'post':
|
||||
if (this.useUpdateInterceptor()) {
|
||||
config.data = this.beforeUpdate(config.data)
|
||||
config.data = objectToSnakeCase(config.data)
|
||||
}
|
||||
break
|
||||
case 'put':
|
||||
if (this.useCreateInterceptor()) {
|
||||
config.data = this.beforeCreate(config.data)
|
||||
config.data = objectToSnakeCase(config.data)
|
||||
}
|
||||
break
|
||||
case 'delete':
|
||||
if (this.useDeleteInterceptor()) {
|
||||
config.data = this.beforeDelete(config.data)
|
||||
config.data = objectToSnakeCase(config.data)
|
||||
}
|
||||
break
|
||||
}
|
||||
return config
|
||||
})
|
||||
|
||||
Object.assign(this.paths, paths)
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not to use the create interceptor which processes a request payload into json
|
||||
*/
|
||||
useCreateInterceptor(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not to use the update interceptor which processes a request payload into json
|
||||
*/
|
||||
useUpdateInterceptor(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not to use the delete interceptor which processes a request payload into json
|
||||
*/
|
||||
useDeleteInterceptor(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
/////////////////
|
||||
// Helper functions
|
||||
///////////////
|
||||
|
||||
/**
|
||||
* Returns an object with all route parameters and their values.
|
||||
* @example
|
||||
* getRouteReplacements(
|
||||
* '/tasks/{taskId}/assignees/{userId}',
|
||||
* { taskId: 7, userId: 2 },
|
||||
* )
|
||||
* // { "{taskId}": 7, "{userId}": 2 }
|
||||
*/
|
||||
getRouteReplacements(route : string, parameters : Record<string, unknown> = {}) {
|
||||
const replace$$1: Record<string, unknown> = {}
|
||||
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.
|
||||
*/
|
||||
getRouteParameterPattern(): RegExp {
|
||||
return /{([^}]+)}/
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a fully-ready-ready-to-make-a-request-to route with replaced parameters.
|
||||
* @example
|
||||
* getReplacedRoute('/projects/{projectId}/tasks', { projectId: 3 })
|
||||
* === '/projects/{projectId}/tasks'
|
||||
*/
|
||||
getReplacedRoute(path : string, pathparams : Record<string, unknown>) : string {
|
||||
const replacements = this.getRouteReplacements(path, pathparams)
|
||||
return Object.entries(replacements).reduce(
|
||||
(result, [parameter, value]) => result.replace(parameter, value as string),
|
||||
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.
|
||||
*/
|
||||
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 a model from an object.
|
||||
* This one here is the default one, usually the service definitions for a model will override this.
|
||||
*/
|
||||
modelFactory(data : Partial<Model>) {
|
||||
return data as Model
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the model factory for get requests.
|
||||
*/
|
||||
modelGetFactory(data : Partial<Model>) {
|
||||
return this.modelFactory(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the model factory for get all requests.
|
||||
*/
|
||||
modelGetAllFactory(data : Partial<Model>) {
|
||||
return this.modelFactory(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the model factory for create requests.
|
||||
*/
|
||||
modelCreateFactory(data : Partial<Model>) {
|
||||
return this.modelFactory(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the model factory for update requests.
|
||||
*/
|
||||
modelUpdateFactory(data : Partial<Model>) {
|
||||
return this.modelFactory(data)
|
||||
}
|
||||
|
||||
//////////////
|
||||
// Preprocessors
|
||||
////////////
|
||||
|
||||
/**
|
||||
* Default preprocessor for get requests
|
||||
*/
|
||||
beforeGet(model : Model) {
|
||||
return model
|
||||
}
|
||||
|
||||
/**
|
||||
* Default preprocessor for create requests
|
||||
*/
|
||||
beforeCreate(model : Model) {
|
||||
return model
|
||||
}
|
||||
|
||||
/**
|
||||
* Default preprocessor for update requests
|
||||
*/
|
||||
beforeUpdate(model : Model) {
|
||||
return model
|
||||
}
|
||||
|
||||
/**
|
||||
* Default preprocessor for delete requests
|
||||
*/
|
||||
beforeDelete(model : 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
|
||||
*/
|
||||
get(model : Model, params = {}) {
|
||||
if (this.paths.get === '') {
|
||||
throw new Error('This model is not able to get data.')
|
||||
}
|
||||
|
||||
return this.getM(this.paths.get, model, params)
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a more abstract implementation which only does a get request.
|
||||
* Services which need more flexibility can use this.
|
||||
*/
|
||||
async getM(url : string, model : Model = new AbstractModel({}), params: Record<string, unknown> = {}) {
|
||||
const cancel = this.setLoading()
|
||||
|
||||
model = this.beforeGet(model)
|
||||
const finalUrl = this.getReplacedRoute(url, model)
|
||||
|
||||
try {
|
||||
const response = await this.http.get(finalUrl, {params: prepareParams(params)})
|
||||
const result = this.modelGetFactory(response.data)
|
||||
result.maxRight = Number(response.headers['x-max-right']) as Right
|
||||
return result
|
||||
} finally {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
async getBlobUrl(url : string, method : Method = 'GET', data = {}) {
|
||||
const response = await this.http({
|
||||
url,
|
||||
method,
|
||||
responseType: 'blob',
|
||||
data,
|
||||
})
|
||||
return window.URL.createObjectURL(new Blob([response.data]))
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param page The page to get
|
||||
*/
|
||||
async getAll(model : Model = new AbstractModel({}), params = {}, page = 1): Promise<Model[]> {
|
||||
if (this.paths.getAll === '') {
|
||||
throw new Error('This model is not able to get data.')
|
||||
}
|
||||
|
||||
params.page = page
|
||||
|
||||
const cancel = this.setLoading()
|
||||
model = this.beforeGet(model)
|
||||
const finalUrl = this.getReplacedRoute(this.paths.getAll, model)
|
||||
|
||||
try {
|
||||
const response = await this.http.get(finalUrl, {params: prepareParams(params)})
|
||||
this.resultCount = Number(response.headers['x-pagination-result-count'])
|
||||
this.totalPages = Number(response.headers['x-pagination-total-pages'])
|
||||
|
||||
if (!Array.isArray(response.data)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return response.data.map(entry => this.modelGetAllFactory(entry))
|
||||
} finally {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a put request to the url specified before
|
||||
* @returns {Promise<any | never>}
|
||||
*/
|
||||
async create(model : Model) {
|
||||
if (this.paths.create === '') {
|
||||
throw new Error('This model is not able to create data.')
|
||||
}
|
||||
|
||||
const cancel = this.setLoading()
|
||||
const finalUrl = this.getReplacedRoute(this.paths.create, model)
|
||||
|
||||
try {
|
||||
const response = await this.http.put(finalUrl, model)
|
||||
const result = this.modelCreateFactory(response.data)
|
||||
if (typeof model.maxRight !== 'undefined') {
|
||||
result.maxRight = model.maxRight
|
||||
}
|
||||
return result
|
||||
} finally {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An abstract implementation to send post requests.
|
||||
* Services can use this to implement functions to do post requests other than using the update method.
|
||||
*/
|
||||
async post(url : string, model : Model) {
|
||||
const cancel = this.setLoading()
|
||||
|
||||
try {
|
||||
const response = await this.http.post(url, model)
|
||||
const result = this.modelUpdateFactory(response.data)
|
||||
if (typeof model.maxRight !== 'undefined') {
|
||||
result.maxRight = model.maxRight
|
||||
}
|
||||
return result
|
||||
} finally {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a post request to the update url
|
||||
*/
|
||||
update(model : Model) {
|
||||
if (this.paths.update === '') {
|
||||
throw new Error('This model is not able to update data.')
|
||||
}
|
||||
|
||||
const finalUrl = this.getReplacedRoute(this.paths.update, model)
|
||||
return this.post(finalUrl, model)
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a delete request to the update url
|
||||
*/
|
||||
async delete(model : Model) {
|
||||
if (this.paths.delete === '') {
|
||||
throw new Error('This model is not able to delete data.')
|
||||
}
|
||||
|
||||
const cancel = this.setLoading()
|
||||
const finalUrl = this.getReplacedRoute(this.paths.delete, model)
|
||||
|
||||
try {
|
||||
const {data} = await this.http.delete(finalUrl, model)
|
||||
return data
|
||||
} finally {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a file to a url.
|
||||
* @param url
|
||||
* @param file {IFile}
|
||||
* @param fieldName The name of the field the file is uploaded to.
|
||||
*/
|
||||
uploadFile(url : string, file: File, fieldName : string) {
|
||||
return this.uploadBlob(url, new Blob([file]), fieldName, file.name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a blob to a url.
|
||||
*/
|
||||
uploadBlob(url : string, blob: Blob, fieldName: string, filename : string) {
|
||||
const data = new FormData()
|
||||
data.append(fieldName, blob, filename)
|
||||
return this.uploadFormData(url, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a form data object.
|
||||
*/
|
||||
async uploadFormData(url : string, formData: FormData) {
|
||||
const cancel = this.setLoading()
|
||||
try {
|
||||
const response = await this.http.put(
|
||||
url,
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type':
|
||||
'multipart/form-data; boundary=' + formData._boundary,
|
||||
},
|
||||
// fix upload issue after upgrading to axios to 1.0.0
|
||||
// see: https://github.com/axios/axios/issues/4885#issuecomment-1222419132
|
||||
transformRequest: formData => formData,
|
||||
onUploadProgress: ({progress}) => {
|
||||
this.uploadProgress = progress? Math.round((progress * 100)) : 0
|
||||
},
|
||||
},
|
||||
)
|
||||
return this.modelCreateFactory(response.data)
|
||||
} finally {
|
||||
this.uploadProgress = 0
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
}
|
15
frontend/src/services/accountDelete.ts
Normal file
15
frontend/src/services/accountDelete.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import AbstractService from './abstractService'
|
||||
|
||||
export default class AccountDeleteService extends AbstractService {
|
||||
request(password: string) {
|
||||
return this.post('/user/deletion/request', {password})
|
||||
}
|
||||
|
||||
confirm(token: string) {
|
||||
return this.post('/user/deletion/confirm', {token})
|
||||
}
|
||||
|
||||
cancel(password: string) {
|
||||
return this.post('/user/deletion/cancel', {password})
|
||||
}
|
||||
}
|
36
frontend/src/services/apiToken.ts
Normal file
36
frontend/src/services/apiToken.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import AbstractService from '@/services/abstractService'
|
||||
import type {IApiToken} from '@/modelTypes/IApiToken'
|
||||
import ApiTokenModel from '@/models/apiTokenModel'
|
||||
|
||||
export default class ApiTokenService extends AbstractService<IApiToken> {
|
||||
constructor() {
|
||||
super({
|
||||
create: '/tokens',
|
||||
getAll: '/tokens',
|
||||
delete: '/tokens/{id}',
|
||||
})
|
||||
}
|
||||
|
||||
processModel(model: IApiToken) {
|
||||
return {
|
||||
...model,
|
||||
expiresAt: new Date(model.expiresAt).toISOString(),
|
||||
created: new Date(model.created).toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
modelFactory(data: Partial<IApiToken>) {
|
||||
return new ApiTokenModel(data)
|
||||
}
|
||||
|
||||
async getAvailableRoutes() {
|
||||
const cancel = this.setLoading()
|
||||
|
||||
try {
|
||||
const response = await this.http.get('/routes')
|
||||
return response.data
|
||||
} finally {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
}
|
66
frontend/src/services/attachment.ts
Normal file
66
frontend/src/services/attachment.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import AbstractService from './abstractService'
|
||||
import AttachmentModel from '../models/attachment'
|
||||
|
||||
import type { IAttachment } from '@/modelTypes/IAttachment'
|
||||
|
||||
import {downloadBlob} from '@/helpers/downloadBlob'
|
||||
|
||||
export default class AttachmentService extends AbstractService<IAttachment> {
|
||||
constructor() {
|
||||
super({
|
||||
create: '/tasks/{taskId}/attachments',
|
||||
getAll: '/tasks/{taskId}/attachments',
|
||||
delete: '/tasks/{taskId}/attachments/{id}',
|
||||
})
|
||||
}
|
||||
|
||||
processModel(model: IAttachment) {
|
||||
return {
|
||||
...model,
|
||||
created: new Date(model.created).toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
useCreateInterceptor() {
|
||||
return false
|
||||
}
|
||||
|
||||
modelFactory(data: Partial<IAttachment>) {
|
||||
return new AttachmentModel(data)
|
||||
}
|
||||
|
||||
modelCreateFactory(data) {
|
||||
// Success contains the uploaded attachments
|
||||
data.success = (data.success === null ? [] : data.success).map(a => {
|
||||
return this.modelFactory(a)
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
getBlobUrl(model: IAttachment) {
|
||||
return AbstractService.prototype.getBlobUrl.call(this, '/tasks/' + model.taskId + '/attachments/' + model.id)
|
||||
}
|
||||
|
||||
async download(model: IAttachment) {
|
||||
const url = await this.getBlobUrl(model)
|
||||
return downloadBlob(url, model.file.name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a file to the server
|
||||
* @param files
|
||||
* @returns {Promise<any|never>}
|
||||
*/
|
||||
create(model: IAttachment, files: File[] | FileList) {
|
||||
const data = new FormData()
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
// TODO: Validation of file size
|
||||
data.append('files', new Blob([files[i]]), files[i].name)
|
||||
}
|
||||
|
||||
return this.uploadFormData(
|
||||
this.getReplacedRoute(this.paths.create, model),
|
||||
data,
|
||||
)
|
||||
}
|
||||
}
|
30
frontend/src/services/avatar.ts
Normal file
30
frontend/src/services/avatar.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import AbstractService from './abstractService'
|
||||
import AvatarModel from '@/models/avatar'
|
||||
import type { IAvatar } from '@/modelTypes/IAvatar'
|
||||
|
||||
export default class AvatarService extends AbstractService<IAvatar> {
|
||||
constructor() {
|
||||
super({
|
||||
get: '/user/settings/avatar',
|
||||
update: '/user/settings/avatar',
|
||||
create: '/user/settings/avatar/upload',
|
||||
})
|
||||
}
|
||||
|
||||
modelFactory(data: Partial<IAvatar>) {
|
||||
return new AvatarModel(data)
|
||||
}
|
||||
|
||||
useCreateInterceptor() {
|
||||
return false
|
||||
}
|
||||
|
||||
create(blob) {
|
||||
return this.uploadBlob(
|
||||
this.paths.create,
|
||||
blob,
|
||||
'avatar',
|
||||
'avatar.jpg', // This fails without a file name
|
||||
)
|
||||
}
|
||||
}
|
30
frontend/src/services/backgroundUnsplash.ts
Normal file
30
frontend/src/services/backgroundUnsplash.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import AbstractService from './abstractService'
|
||||
import BackgroundImageModel from '../models/backgroundImage'
|
||||
import ProjectModel from '@/models/project'
|
||||
import type { IBackgroundImage } from '@/modelTypes/IBackgroundImage'
|
||||
|
||||
export default class BackgroundUnsplashService extends AbstractService<IBackgroundImage> {
|
||||
constructor() {
|
||||
super({
|
||||
getAll: '/backgrounds/unsplash/search',
|
||||
update: '/projects/{projectId}/backgrounds/unsplash',
|
||||
})
|
||||
}
|
||||
|
||||
modelFactory(data: Partial<IBackgroundImage>) {
|
||||
return new BackgroundImageModel(data)
|
||||
}
|
||||
|
||||
modelUpdateFactory(data) {
|
||||
return new ProjectModel(data)
|
||||
}
|
||||
|
||||
async thumb(model) {
|
||||
const response = await this.http({
|
||||
url: `/backgrounds/unsplash/images/${model.id}/thumb`,
|
||||
method: 'GET',
|
||||
responseType: 'blob',
|
||||
})
|
||||
return window.URL.createObjectURL(new Blob([response.data]))
|
||||
}
|
||||
}
|
32
frontend/src/services/backgroundUpload.ts
Normal file
32
frontend/src/services/backgroundUpload.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import AbstractService from './abstractService'
|
||||
import ProjectModel from '@/models/project'
|
||||
|
||||
import type { IProject } from '@/modelTypes/IProject'
|
||||
import type { IFile } from '@/modelTypes/IFile'
|
||||
|
||||
export default class BackgroundUploadService extends AbstractService {
|
||||
constructor() {
|
||||
super({
|
||||
create: '/projects/{projectId}/backgrounds/upload',
|
||||
})
|
||||
}
|
||||
|
||||
useCreateInterceptor() {
|
||||
return false
|
||||
}
|
||||
|
||||
modelCreateFactory(data: Partial<IProject>) {
|
||||
return new ProjectModel(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a file to the server
|
||||
*/
|
||||
create(projectId: IProject['id'], file: IFile) {
|
||||
return this.uploadFile(
|
||||
this.getReplacedRoute(this.paths.create, {projectId}),
|
||||
file,
|
||||
'background',
|
||||
)
|
||||
}
|
||||
}
|
25
frontend/src/services/bucket.ts
Normal file
25
frontend/src/services/bucket.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import AbstractService from './abstractService'
|
||||
import BucketModel from '../models/bucket'
|
||||
import TaskService from '@/services/task'
|
||||
import type { IBucket } from '@/modelTypes/IBucket'
|
||||
|
||||
export default class BucketService extends AbstractService<IBucket> {
|
||||
constructor() {
|
||||
super({
|
||||
getAll: '/projects/{projectId}/buckets',
|
||||
create: '/projects/{projectId}/buckets',
|
||||
update: '/projects/{projectId}/buckets/{id}',
|
||||
delete: '/projects/{projectId}/buckets/{id}',
|
||||
})
|
||||
}
|
||||
|
||||
modelFactory(data: Partial<IBucket>) {
|
||||
return new BucketModel(data)
|
||||
}
|
||||
|
||||
beforeUpdate(model) {
|
||||
const taskService = new TaskService()
|
||||
model.tasks = model.tasks?.map(t => taskService.processModel(t))
|
||||
return model
|
||||
}
|
||||
}
|
17
frontend/src/services/caldavToken.ts
Normal file
17
frontend/src/services/caldavToken.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import CaldavTokenModel from '@/models/caldavToken'
|
||||
import type {ICaldavToken} from '@/modelTypes/ICaldavToken'
|
||||
import AbstractService from './abstractService'
|
||||
|
||||
export default class CaldavTokenService extends AbstractService<ICaldavToken> {
|
||||
constructor() {
|
||||
super({
|
||||
getAll: '/user/settings/token/caldav',
|
||||
create: '/user/settings/token/caldav',
|
||||
delete: '/user/settings/token/caldav/{id}',
|
||||
})
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new CaldavTokenModel(data)
|
||||
}
|
||||
}
|
20
frontend/src/services/dataExport.ts
Normal file
20
frontend/src/services/dataExport.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import AbstractService from './abstractService'
|
||||
import {downloadBlob} from '../helpers/downloadBlob'
|
||||
|
||||
const DOWNLOAD_NAME = 'vikunja-export.zip'
|
||||
|
||||
export default class DataExportService extends AbstractService {
|
||||
request(password: string) {
|
||||
return this.post('/user/export/request', {password})
|
||||
}
|
||||
|
||||
async download(password: string) {
|
||||
const clear = this.setLoading()
|
||||
try {
|
||||
const url = await this.getBlobUrl('/user/export/download', 'POST', {password})
|
||||
downloadBlob(url, DOWNLOAD_NAME)
|
||||
} finally {
|
||||
clear()
|
||||
}
|
||||
}
|
||||
}
|
9
frontend/src/services/emailUpdate.ts
Normal file
9
frontend/src/services/emailUpdate.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import AbstractService from './abstractService'
|
||||
|
||||
export default class EmailUpdateService extends AbstractService {
|
||||
constructor() {
|
||||
super({
|
||||
update: '/user/settings/email',
|
||||
})
|
||||
}
|
||||
}
|
35
frontend/src/services/label.ts
Normal file
35
frontend/src/services/label.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import AbstractService from './abstractService'
|
||||
import LabelModel from '@/models/label'
|
||||
import type {ILabel} from '@/modelTypes/ILabel'
|
||||
import {colorFromHex} from '@/helpers/color/colorFromHex'
|
||||
|
||||
export default class LabelService extends AbstractService<ILabel> {
|
||||
constructor() {
|
||||
super({
|
||||
create: '/labels',
|
||||
getAll: '/labels',
|
||||
get: '/labels/{id}',
|
||||
update: '/labels/{id}',
|
||||
delete: '/labels/{id}',
|
||||
})
|
||||
}
|
||||
|
||||
processModel(label) {
|
||||
label.created = new Date(label.created).toISOString()
|
||||
label.updated = new Date(label.updated).toISOString()
|
||||
label.hexColor = colorFromHex(label.hexColor)
|
||||
return label
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new LabelModel(data)
|
||||
}
|
||||
|
||||
beforeUpdate(label) {
|
||||
return this.processModel(label)
|
||||
}
|
||||
|
||||
beforeCreate(label) {
|
||||
return this.processModel(label)
|
||||
}
|
||||
}
|
17
frontend/src/services/labelTask.ts
Normal file
17
frontend/src/services/labelTask.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import AbstractService from './abstractService'
|
||||
import LabelTask from '@/models/labelTask'
|
||||
import type {ILabelTask} from '@/modelTypes/ILabelTask'
|
||||
|
||||
export default class LabelTaskService extends AbstractService<ILabelTask> {
|
||||
constructor() {
|
||||
super({
|
||||
create: '/tasks/{taskId}/labels',
|
||||
getAll: '/tasks/{taskId}/labels',
|
||||
delete: '/tasks/{taskId}/labels/{labelId}',
|
||||
})
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new LabelTask(data)
|
||||
}
|
||||
}
|
18
frontend/src/services/linkShare.ts
Normal file
18
frontend/src/services/linkShare.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import AbstractService from './abstractService'
|
||||
import LinkShareModel from '@/models/linkShare'
|
||||
import type {ILinkShare} from '@/modelTypes/ILinkShare'
|
||||
|
||||
export default class LinkShareService extends AbstractService<ILinkShare> {
|
||||
constructor() {
|
||||
super({
|
||||
getAll: '/projects/{projectId}/shares',
|
||||
get: '/projects/{projectId}/shares/{id}',
|
||||
create: '/projects/{projectId}/shares',
|
||||
delete: '/projects/{projectId}/shares/{id}',
|
||||
})
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new LinkShareModel(data)
|
||||
}
|
||||
}
|
28
frontend/src/services/migrator/abstractMigration.ts
Normal file
28
frontend/src/services/migrator/abstractMigration.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import AbstractService from '../abstractService'
|
||||
|
||||
export type MigrationConfig = { code: string }
|
||||
|
||||
// This service builds on top of the abstract service and basically just hides away method names.
|
||||
// It enables migration services to be created with minimal overhead and even better method names.
|
||||
export default class AbstractMigrationService extends AbstractService<MigrationConfig> {
|
||||
serviceUrlKey = ''
|
||||
|
||||
constructor(serviceUrlKey: string) {
|
||||
super({
|
||||
update: '/migration/' + serviceUrlKey + '/migrate',
|
||||
})
|
||||
this.serviceUrlKey = serviceUrlKey
|
||||
}
|
||||
|
||||
getAuthUrl() {
|
||||
return this.getM('/migration/' + this.serviceUrlKey + '/auth')
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
return this.getM('/migration/' + this.serviceUrlKey + '/status')
|
||||
}
|
||||
|
||||
migrate(data: MigrationConfig) {
|
||||
return this.update(data)
|
||||
}
|
||||
}
|
30
frontend/src/services/migrator/abstractMigrationFile.ts
Normal file
30
frontend/src/services/migrator/abstractMigrationFile.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import AbstractService from '../abstractService'
|
||||
|
||||
// This service builds on top of the abstract service and basically just hides away method names.
|
||||
// It enables migration services to be created with minimal overhead and even better method names.
|
||||
export default class AbstractMigrationFileService extends AbstractService {
|
||||
serviceUrlKey = ''
|
||||
|
||||
constructor(serviceUrlKey: string) {
|
||||
super({
|
||||
create: '/migration/' + serviceUrlKey + '/migrate',
|
||||
})
|
||||
this.serviceUrlKey = serviceUrlKey
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
return this.getM('/migration/' + this.serviceUrlKey + '/status')
|
||||
}
|
||||
|
||||
useCreateInterceptor() {
|
||||
return false
|
||||
}
|
||||
|
||||
migrate(file: File) {
|
||||
return this.uploadFile(
|
||||
this.paths.create,
|
||||
file,
|
||||
'import',
|
||||
)
|
||||
}
|
||||
}
|
30
frontend/src/services/notification.ts
Normal file
30
frontend/src/services/notification.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import AbstractService from '@/services/abstractService'
|
||||
import NotificationModel from '@/models/notification'
|
||||
import type {INotification} from '@/modelTypes/INotification'
|
||||
|
||||
export default class NotificationService extends AbstractService<INotification> {
|
||||
constructor() {
|
||||
super({
|
||||
getAll: '/notifications',
|
||||
update: '/notifications/{id}',
|
||||
})
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new NotificationModel(data)
|
||||
}
|
||||
|
||||
beforeUpdate(model) {
|
||||
if (!model) {
|
||||
return model
|
||||
}
|
||||
|
||||
model.created = new Date(model.created).toISOString()
|
||||
model.readAt = new Date(model.readAt).toISOString()
|
||||
return model
|
||||
}
|
||||
|
||||
async markAllRead() {
|
||||
return this.post('/notifications', false)
|
||||
}
|
||||
}
|
38
frontend/src/services/passwordReset.ts
Normal file
38
frontend/src/services/passwordReset.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import AbstractService from './abstractService'
|
||||
import PasswordResetModel from '@/models/passwordReset'
|
||||
import type {IPasswordReset} from '@/modelTypes/IPasswordReset'
|
||||
|
||||
export default class PasswordResetService extends AbstractService<IPasswordReset> {
|
||||
|
||||
constructor() {
|
||||
super({})
|
||||
this.paths = {
|
||||
reset: '/user/password/reset',
|
||||
requestReset: '/user/password/token',
|
||||
}
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new PasswordResetModel(data)
|
||||
}
|
||||
|
||||
async resetPassword(model) {
|
||||
const cancel = this.setLoading()
|
||||
try {
|
||||
const response = await this.http.post(this.paths.reset, model)
|
||||
return this.modelFactory(response.data)
|
||||
} finally {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
async requestResetPassword(model) {
|
||||
const cancel = this.setLoading()
|
||||
try {
|
||||
const response = await this.http.post(this.paths.requestReset, model)
|
||||
return this.modelFactory(response.data)
|
||||
} finally {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
}
|
10
frontend/src/services/passwordUpdateService.ts
Normal file
10
frontend/src/services/passwordUpdateService.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import AbstractService from './abstractService'
|
||||
import type {IPasswordUpdate} from '@/modelTypes/IPasswordUpdate'
|
||||
|
||||
export default class PasswordUpdateService extends AbstractService<IPasswordUpdate> {
|
||||
constructor() {
|
||||
super({
|
||||
update: '/user/password',
|
||||
})
|
||||
}
|
||||
}
|
65
frontend/src/services/project.ts
Normal file
65
frontend/src/services/project.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import AbstractService from './abstractService'
|
||||
import ProjectModel from '@/models/project'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
import TaskService from './task'
|
||||
import {colorFromHex} from '@/helpers/color/colorFromHex'
|
||||
|
||||
export default class ProjectService extends AbstractService<IProject> {
|
||||
constructor() {
|
||||
super({
|
||||
create: '/projects',
|
||||
get: '/projects/{id}',
|
||||
getAll: '/projects',
|
||||
update: '/projects/{id}',
|
||||
delete: '/projects/{id}',
|
||||
})
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new ProjectModel(data)
|
||||
}
|
||||
|
||||
beforeUpdate(model) {
|
||||
if(typeof model.tasks !== 'undefined') {
|
||||
const taskService = new TaskService()
|
||||
model.tasks = model.tasks.map(task => {
|
||||
return taskService.beforeUpdate(task)
|
||||
})
|
||||
}
|
||||
|
||||
if(typeof model.hexColor !== 'undefined') {
|
||||
model.hexColor = colorFromHex(model.hexColor)
|
||||
}
|
||||
|
||||
return model
|
||||
}
|
||||
|
||||
beforeCreate(project) {
|
||||
project.hexColor = colorFromHex(project.hexColor)
|
||||
return project
|
||||
}
|
||||
|
||||
async background(project: Pick<IProject, 'id' | 'backgroundInformation'>) {
|
||||
if (project.backgroundInformation === null) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const response = await this.http({
|
||||
url: `/projects/${project.id}/background`,
|
||||
method: 'GET',
|
||||
responseType: 'blob',
|
||||
})
|
||||
return window.URL.createObjectURL(new Blob([response.data]))
|
||||
}
|
||||
|
||||
async removeBackground(project: Pick<IProject, 'id'>) {
|
||||
const cancel = this.setLoading()
|
||||
|
||||
try {
|
||||
const response = await this.http.delete(`/projects/${project.id}/background`, project)
|
||||
return response.data
|
||||
} finally {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
}
|
21
frontend/src/services/projectDuplicateService.ts
Normal file
21
frontend/src/services/projectDuplicateService.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import AbstractService from './abstractService'
|
||||
import projectDuplicateModel from '@/models/projectDuplicateModel'
|
||||
import type {IProjectDuplicate} from '@/modelTypes/IProjectDuplicate'
|
||||
|
||||
export default class ProjectDuplicateService extends AbstractService<IProjectDuplicate> {
|
||||
constructor() {
|
||||
super({
|
||||
create: '/projects/{projectId}/duplicate',
|
||||
})
|
||||
}
|
||||
|
||||
beforeCreate(model) {
|
||||
|
||||
model.project = null
|
||||
return model
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new projectDuplicateModel(data)
|
||||
}
|
||||
}
|
14
frontend/src/services/projectUsers.ts
Normal file
14
frontend/src/services/projectUsers.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import AbstractService from './abstractService'
|
||||
import UserModel from '../models/user'
|
||||
|
||||
export default class ProjectUserService extends AbstractService {
|
||||
constructor() {
|
||||
super({
|
||||
getAll: '/projects/{projectId}/projectusers',
|
||||
})
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new UserModel(data)
|
||||
}
|
||||
}
|
172
frontend/src/services/savedFilter.ts
Normal file
172
frontend/src/services/savedFilter.ts
Normal file
@ -0,0 +1,172 @@
|
||||
import {computed, ref, shallowReactive, unref, watch} from 'vue'
|
||||
import {useRouter} from 'vue-router'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import type {MaybeRef} from '@vueuse/core'
|
||||
import {useDebounceFn} from '@vueuse/core'
|
||||
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
import type {ISavedFilter} from '@/modelTypes/ISavedFilter'
|
||||
|
||||
import AbstractService from '@/services/abstractService'
|
||||
|
||||
import SavedFilterModel from '@/models/savedFilter'
|
||||
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
|
||||
import {objectToSnakeCase, objectToCamelCase} from '@/helpers/case'
|
||||
import {success} from '@/message'
|
||||
import ProjectModel from '@/models/project'
|
||||
|
||||
/**
|
||||
* Calculates the corresponding project id to this saved filter.
|
||||
* This function matches the one in the api.
|
||||
*/
|
||||
function getProjectId(savedFilter: ISavedFilter) {
|
||||
let projectId = savedFilter.id * -1 - 1
|
||||
if (projectId > 0) {
|
||||
projectId = 0
|
||||
}
|
||||
return projectId
|
||||
}
|
||||
|
||||
export function getSavedFilterIdFromProjectId(projectId: IProject['id']) {
|
||||
let filterId = projectId * -1 - 1
|
||||
// FilterIds from projectIds are always positive
|
||||
if (filterId < 0) {
|
||||
filterId = 0
|
||||
}
|
||||
return filterId
|
||||
}
|
||||
|
||||
export function isSavedFilter(project: IProject) {
|
||||
return getSavedFilterIdFromProjectId(project?.id) > 0
|
||||
}
|
||||
|
||||
export default class SavedFilterService extends AbstractService<ISavedFilter> {
|
||||
constructor() {
|
||||
super({
|
||||
get: '/filters/{id}',
|
||||
create: '/filters',
|
||||
update: '/filters/{id}',
|
||||
delete: '/filters/{id}',
|
||||
})
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new SavedFilterModel(data)
|
||||
}
|
||||
|
||||
processModel(model) {
|
||||
// Make filters from this.filters camelCase and set them to the model property:
|
||||
// That's easier than making the whole filter component configurable since that still needs to provide
|
||||
// the filter values in snake_sćase for url parameters.
|
||||
model.filters = objectToCamelCase(model.filters)
|
||||
|
||||
// Make sure all filterValues are passes as strings. This is a requirement of the api.
|
||||
model.filters.filterValue = model.filters.filterValue.map(v => String(v))
|
||||
|
||||
return model
|
||||
}
|
||||
|
||||
beforeUpdate(model) {
|
||||
return this.processModel(model)
|
||||
}
|
||||
|
||||
beforeCreate(model) {
|
||||
return this.processModel(model)
|
||||
}
|
||||
}
|
||||
|
||||
export function useSavedFilter(projectId?: MaybeRef<IProject['id']>) {
|
||||
const router = useRouter()
|
||||
const {t} = useI18n({useScope:'global'})
|
||||
const projectStore = useProjectStore()
|
||||
|
||||
const filterService = shallowReactive(new SavedFilterService())
|
||||
|
||||
const filter = ref<ISavedFilter>(new SavedFilterModel())
|
||||
const filters = computed({
|
||||
get: () => filter.value.filters,
|
||||
set(value) {
|
||||
filter.value.filters = value
|
||||
},
|
||||
})
|
||||
|
||||
// load SavedFilter
|
||||
watch(() => unref(projectId), async (watchedProjectId) => {
|
||||
if (watchedProjectId === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
// We assume the projectId in the route is the pseudoproject
|
||||
const savedFilterId = getSavedFilterIdFromProjectId(watchedProjectId)
|
||||
|
||||
filter.value = new SavedFilterModel({id: savedFilterId})
|
||||
const response = await filterService.get(filter.value)
|
||||
response.filters = objectToSnakeCase(response.filters)
|
||||
filter.value = response
|
||||
await validateTitleField()
|
||||
}, {immediate: true})
|
||||
|
||||
async function createFilter() {
|
||||
filter.value = await filterService.create(filter.value)
|
||||
await projectStore.loadProjects()
|
||||
router.push({name: 'project.index', params: {projectId: getProjectId(filter.value)}})
|
||||
}
|
||||
|
||||
async function saveFilter() {
|
||||
const response = await filterService.update(filter.value)
|
||||
await projectStore.loadProjects()
|
||||
success({message: t('filters.edit.success')})
|
||||
response.filters = objectToSnakeCase(response.filters)
|
||||
filter.value = response
|
||||
await useBaseStore().setCurrentProject(new ProjectModel({
|
||||
id: getProjectId(filter.value),
|
||||
title: filter.value.title,
|
||||
}))
|
||||
router.back()
|
||||
}
|
||||
|
||||
async function deleteFilter() {
|
||||
await filterService.delete(filter.value)
|
||||
await projectStore.loadProjects()
|
||||
success({message: t('filters.delete.success')})
|
||||
router.push({name: 'projects.index'})
|
||||
}
|
||||
|
||||
const titleValid = ref(true)
|
||||
const validateTitleField = useDebounceFn(() => {
|
||||
titleValid.value = filter.value.title !== ''
|
||||
}, 100)
|
||||
|
||||
async function createFilterWithValidation() {
|
||||
if (!titleValid.value) {
|
||||
return
|
||||
}
|
||||
return createFilter()
|
||||
}
|
||||
|
||||
async function saveFilterWithValidation() {
|
||||
if (!titleValid.value) {
|
||||
return
|
||||
}
|
||||
return saveFilter()
|
||||
}
|
||||
|
||||
return {
|
||||
createFilter,
|
||||
createFilterWithValidation,
|
||||
saveFilter,
|
||||
saveFilterWithValidation,
|
||||
deleteFilter,
|
||||
|
||||
filter,
|
||||
filters,
|
||||
|
||||
filterService,
|
||||
|
||||
titleValid,
|
||||
validateTitleField,
|
||||
}
|
||||
}
|
16
frontend/src/services/subscription.ts
Normal file
16
frontend/src/services/subscription.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import AbstractService from '@/services/abstractService'
|
||||
import SubscriptionModel from '@/models/subscription'
|
||||
import type {ISubscription} from '@/modelTypes/ISubscription'
|
||||
|
||||
export default class SubscriptionService extends AbstractService<ISubscription> {
|
||||
constructor() {
|
||||
super({
|
||||
create: '/subscriptions/{entity}/{entityId}',
|
||||
delete: '/subscriptions/{entity}/{entityId}',
|
||||
})
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new SubscriptionModel(data)
|
||||
}
|
||||
}
|
114
frontend/src/services/task.ts
Normal file
114
frontend/src/services/task.ts
Normal file
@ -0,0 +1,114 @@
|
||||
import AbstractService from './abstractService'
|
||||
import TaskModel from '@/models/task'
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
import AttachmentService from './attachment'
|
||||
import LabelService from './label'
|
||||
|
||||
import {colorFromHex} from '@/helpers/color/colorFromHex'
|
||||
import {SECONDS_A_DAY, SECONDS_A_HOUR, SECONDS_A_WEEK} from '@/constants/date'
|
||||
|
||||
const parseDate = date => {
|
||||
if (date) {
|
||||
return new Date(date).toISOString()
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default class TaskService extends AbstractService<ITask> {
|
||||
constructor() {
|
||||
super({
|
||||
create: '/projects/{projectId}/tasks',
|
||||
getAll: '/tasks/all',
|
||||
get: '/tasks/{id}',
|
||||
update: '/tasks/{id}',
|
||||
delete: '/tasks/{id}',
|
||||
})
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new TaskModel(data)
|
||||
}
|
||||
|
||||
beforeUpdate(model) {
|
||||
return this.processModel(model)
|
||||
}
|
||||
|
||||
beforeCreate(model) {
|
||||
return this.processModel(model)
|
||||
}
|
||||
|
||||
processModel(updatedModel) {
|
||||
const model = { ...updatedModel }
|
||||
|
||||
model.title = model.title?.trim()
|
||||
|
||||
// Ensure that projectId is an int
|
||||
model.projectId = Number(model.projectId)
|
||||
|
||||
// Convert dates into an iso string
|
||||
model.dueDate = parseDate(model.dueDate)
|
||||
model.startDate = parseDate(model.startDate)
|
||||
model.endDate = parseDate(model.endDate)
|
||||
model.doneAt = parseDate(model.doneAt)
|
||||
model.created = new Date(model.created).toISOString()
|
||||
model.updated = new Date(model.updated).toISOString()
|
||||
|
||||
model.reminderDates = null
|
||||
// remove all nulls, these would create empty reminders
|
||||
for (const index in model.reminders) {
|
||||
if (model.reminders[index] === null) {
|
||||
model.reminders.splice(index, 1)
|
||||
}
|
||||
}
|
||||
// Make normal timestamps from js dates
|
||||
if (model.reminders.length > 0) {
|
||||
model.reminders.forEach(r => {
|
||||
r.reminder = new Date(r.reminder).toISOString()
|
||||
})
|
||||
}
|
||||
|
||||
// Make the repeating amount to seconds
|
||||
let repeatAfterSeconds = 0
|
||||
if (model.repeatAfter !== null && (model.repeatAfter.amount !== null || model.repeatAfter.amount !== 0)) {
|
||||
switch (model.repeatAfter.type) {
|
||||
case 'hours':
|
||||
repeatAfterSeconds = model.repeatAfter.amount * SECONDS_A_HOUR
|
||||
break
|
||||
case 'days':
|
||||
repeatAfterSeconds = model.repeatAfter.amount * SECONDS_A_DAY
|
||||
break
|
||||
case 'weeks':
|
||||
repeatAfterSeconds = model.repeatAfter.amount * SECONDS_A_WEEK
|
||||
break
|
||||
}
|
||||
}
|
||||
model.repeatAfter = repeatAfterSeconds
|
||||
|
||||
model.hexColor = colorFromHex(model.hexColor)
|
||||
|
||||
// Do the same for all related tasks
|
||||
Object.keys(model.relatedTasks).forEach(relationKind => {
|
||||
model.relatedTasks[relationKind] = model.relatedTasks[relationKind].map(t => {
|
||||
return this.processModel(t)
|
||||
})
|
||||
})
|
||||
|
||||
// Process all attachments to preven parsing errors
|
||||
if (model.attachments.length > 0) {
|
||||
const attachmentService = new AttachmentService()
|
||||
model.attachments.map(a => {
|
||||
return attachmentService.processModel(a)
|
||||
})
|
||||
}
|
||||
|
||||
// Preprocess all labels
|
||||
if (model.labels.length > 0) {
|
||||
const labelService = new LabelService()
|
||||
model.labels = model.labels.map(l => labelService.processModel(l))
|
||||
}
|
||||
|
||||
return model as ITask
|
||||
}
|
||||
}
|
||||
|
16
frontend/src/services/taskAssignee.ts
Normal file
16
frontend/src/services/taskAssignee.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import AbstractService from './abstractService'
|
||||
import TaskAssigneeModel from '@/models/taskAssignee'
|
||||
import type {ITaskAssignee} from '@/modelTypes/ITaskAssignee'
|
||||
|
||||
export default class TaskAssigneeService extends AbstractService<ITaskAssignee> {
|
||||
constructor() {
|
||||
super({
|
||||
create: '/tasks/{taskId}/assignees',
|
||||
delete: '/tasks/{taskId}/assignees/{userId}',
|
||||
})
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new TaskAssigneeModel(data)
|
||||
}
|
||||
}
|
27
frontend/src/services/taskCollection.ts
Normal file
27
frontend/src/services/taskCollection.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import AbstractService from '@/services/abstractService'
|
||||
import TaskModel from '@/models/task'
|
||||
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
|
||||
// FIXME: unite with other filter params types
|
||||
export interface GetAllTasksParams {
|
||||
sort_by: ('start_date' | 'done' | 'id')[],
|
||||
order_by: ('asc' | 'asc' | 'desc')[],
|
||||
filter_by: 'start_date'[],
|
||||
filter_comparator: ('greater_equals' | 'less_equals')[],
|
||||
filter_value: [string, string] // [dateFrom, dateTo],
|
||||
filter_concat: 'and',
|
||||
filter_include_nulls: boolean,
|
||||
}
|
||||
|
||||
export default class TaskCollectionService extends AbstractService<ITask> {
|
||||
constructor() {
|
||||
super({
|
||||
getAll: '/projects/{projectId}/tasks',
|
||||
})
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new TaskModel(data)
|
||||
}
|
||||
}
|
19
frontend/src/services/taskComment.ts
Normal file
19
frontend/src/services/taskComment.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import AbstractService from './abstractService'
|
||||
import TaskCommentModel from '@/models/taskComment'
|
||||
import type {ITaskComment} from '@/modelTypes/ITaskComment'
|
||||
|
||||
export default class TaskCommentService extends AbstractService<ITaskComment> {
|
||||
constructor() {
|
||||
super({
|
||||
create: '/tasks/{taskId}/comments',
|
||||
getAll: '/tasks/{taskId}/comments',
|
||||
get: '/tasks/{taskId}/comments/{id}',
|
||||
update: '/tasks/{taskId}/comments/{id}',
|
||||
delete: '/tasks/{taskId}/comments/{id}',
|
||||
})
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new TaskCommentModel(data)
|
||||
}
|
||||
}
|
16
frontend/src/services/taskRelation.ts
Normal file
16
frontend/src/services/taskRelation.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import AbstractService from './abstractService'
|
||||
import TaskRelationModel from '@/models/taskRelation'
|
||||
import type {ITaskRelation} from '@/modelTypes/ITaskRelation'
|
||||
|
||||
export default class TaskRelationService extends AbstractService<ITaskRelation> {
|
||||
constructor() {
|
||||
super({
|
||||
create: '/tasks/{taskId}/relations',
|
||||
delete: '/tasks/{taskId}/relations/{relationKind}/{otherTaskId}',
|
||||
})
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new TaskRelationModel(data)
|
||||
}
|
||||
}
|
19
frontend/src/services/team.ts
Normal file
19
frontend/src/services/team.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import AbstractService from './abstractService'
|
||||
import TeamModel from '@/models/team'
|
||||
import type {ITeam} from '@/modelTypes/ITeam'
|
||||
|
||||
export default class TeamService extends AbstractService<ITeam> {
|
||||
constructor() {
|
||||
super({
|
||||
create: '/teams',
|
||||
get: '/teams/{id}',
|
||||
getAll: '/teams',
|
||||
update: '/teams/{id}',
|
||||
delete: '/teams/{id}',
|
||||
})
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new TeamModel(data)
|
||||
}
|
||||
}
|
23
frontend/src/services/teamMember.ts
Normal file
23
frontend/src/services/teamMember.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import AbstractService from './abstractService'
|
||||
import TeamMemberModel from '@/models/teamMember'
|
||||
import type {ITeamMember} from '@/modelTypes/ITeamMember'
|
||||
|
||||
export default class TeamMemberService extends AbstractService<ITeamMember> {
|
||||
constructor() {
|
||||
super({
|
||||
create: '/teams/{teamId}/members',
|
||||
delete: '/teams/{teamId}/members/{username}',
|
||||
update: '/teams/{teamId}/members/{username}/admin',
|
||||
})
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new TeamMemberModel(data)
|
||||
}
|
||||
|
||||
beforeCreate(model) {
|
||||
model.userId = model.id // The api wants to get the user id as user_Id
|
||||
model.admin = model.admin === null ? false : model.admin
|
||||
return model
|
||||
}
|
||||
}
|
23
frontend/src/services/teamProject.ts
Normal file
23
frontend/src/services/teamProject.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import AbstractService from './abstractService'
|
||||
import TeamProjectModel from '@/models/teamProject'
|
||||
import type {ITeamProject} from '@/modelTypes/ITeamProject'
|
||||
import TeamModel from '@/models/team'
|
||||
|
||||
export default class TeamProjectService extends AbstractService<ITeamProject> {
|
||||
constructor() {
|
||||
super({
|
||||
create: '/projects/{projectId}/teams',
|
||||
getAll: '/projects/{projectId}/teams',
|
||||
update: '/projects/{projectId}/teams/{teamId}',
|
||||
delete: '/projects/{projectId}/teams/{teamId}',
|
||||
})
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new TeamProjectModel(data)
|
||||
}
|
||||
|
||||
modelGetAllFactory(data) {
|
||||
return new TeamModel(data)
|
||||
}
|
||||
}
|
38
frontend/src/services/totp.ts
Normal file
38
frontend/src/services/totp.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import AbstractService from './abstractService'
|
||||
import TotpModel from '@/models/totp'
|
||||
import type {ITotp} from '@/modelTypes/ITotp'
|
||||
|
||||
export default class TotpService extends AbstractService<ITotp> {
|
||||
urlPrefix = '/user/settings/totp'
|
||||
|
||||
constructor() {
|
||||
super({})
|
||||
|
||||
this.paths.get = this.urlPrefix
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new TotpModel(data)
|
||||
}
|
||||
|
||||
enroll() {
|
||||
return this.post(`${this.urlPrefix}/enroll`, {})
|
||||
}
|
||||
|
||||
enable(model) {
|
||||
return this.post(`${this.urlPrefix}/enable`, model)
|
||||
}
|
||||
|
||||
disable(model) {
|
||||
return this.post(`${this.urlPrefix}/disable`, model)
|
||||
}
|
||||
|
||||
async qrcode() {
|
||||
const response = await this.http({
|
||||
url: `${this.urlPrefix}/qrcode`,
|
||||
method: 'GET',
|
||||
responseType: 'blob',
|
||||
})
|
||||
return new Blob([response.data])
|
||||
}
|
||||
}
|
15
frontend/src/services/user.ts
Normal file
15
frontend/src/services/user.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import AbstractService from './abstractService'
|
||||
import UserModel from '@/models/user'
|
||||
import type {IUser} from '@/modelTypes/IUser'
|
||||
|
||||
export default class UserService extends AbstractService<IUser> {
|
||||
constructor() {
|
||||
super({
|
||||
getAll: '/users',
|
||||
})
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new UserModel(data)
|
||||
}
|
||||
}
|
23
frontend/src/services/userProject.ts
Normal file
23
frontend/src/services/userProject.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import AbstractService from './abstractService'
|
||||
import UserProjectModel from '@/models/userProject'
|
||||
import type {IUserProject} from '@/modelTypes/IUserProject'
|
||||
import UserModel from '@/models/user'
|
||||
|
||||
export default class UserProjectService extends AbstractService<IUserProject> {
|
||||
constructor() {
|
||||
super({
|
||||
create: '/projects/{projectId}/users',
|
||||
getAll: '/projects/{projectId}/users',
|
||||
update: '/projects/{projectId}/users/{userId}',
|
||||
delete: '/projects/{projectId}/users/{userId}',
|
||||
})
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new UserProjectModel(data)
|
||||
}
|
||||
|
||||
modelGetAllFactory(data) {
|
||||
return new UserModel(data)
|
||||
}
|
||||
}
|
10
frontend/src/services/userSettings.ts
Normal file
10
frontend/src/services/userSettings.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import type {IUserSettings} from '@/modelTypes/IUserSettings'
|
||||
import AbstractService from './abstractService'
|
||||
|
||||
export default class UserSettingsService extends AbstractService<IUserSettings> {
|
||||
constructor() {
|
||||
super({
|
||||
update: '/user/settings/general',
|
||||
})
|
||||
}
|
||||
}
|
29
frontend/src/services/webhook.ts
Normal file
29
frontend/src/services/webhook.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import AbstractService from '@/services/abstractService'
|
||||
import type {IWebhook} from '@/modelTypes/IWebhook'
|
||||
import WebhookModel from '@/models/webhook'
|
||||
|
||||
export default class WebhookService extends AbstractService<IWebhook> {
|
||||
constructor() {
|
||||
super({
|
||||
getAll: '/projects/{projectId}/webhooks',
|
||||
create: '/projects/{projectId}/webhooks',
|
||||
update: '/projects/{projectId}/webhooks/{id}',
|
||||
delete: '/projects/{projectId}/webhooks/{id}',
|
||||
})
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new WebhookModel(data)
|
||||
}
|
||||
|
||||
async getAvailableEvents(): Promise<string[]> {
|
||||
const cancel = this.setLoading()
|
||||
|
||||
try {
|
||||
const response = await this.http.get('/webhooks/events')
|
||||
return response.data
|
||||
} finally {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user