feat: emoji reactions for tasks and comments (#2196)
This PR adds reactions for tasks and comments, similar to what you can do on Gitea, GitHub, Slack and plenty of other tools. Reviewed-on: https://kolaente.dev/vikunja/vikunja/pulls/2196 Co-authored-by: kolaente <k@knt.li> Co-committed-by: kolaente <k@knt.li>
This commit is contained in:
@ -77,19 +77,25 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
||||
case 'post':
|
||||
if (this.useUpdateInterceptor()) {
|
||||
config.data = this.beforeUpdate(config.data)
|
||||
config.data = objectToSnakeCase(config.data)
|
||||
if(this.autoTransformBeforePost()) {
|
||||
config.data = objectToSnakeCase(config.data)
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'put':
|
||||
if (this.useCreateInterceptor()) {
|
||||
config.data = this.beforeCreate(config.data)
|
||||
config.data = objectToSnakeCase(config.data)
|
||||
if(this.autoTransformBeforePut()) {
|
||||
config.data = objectToSnakeCase(config.data)
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'delete':
|
||||
if (this.useDeleteInterceptor()) {
|
||||
config.data = this.beforeDelete(config.data)
|
||||
config.data = objectToSnakeCase(config.data)
|
||||
if(this.autoTransformBeforeDelete()) {
|
||||
config.data = objectToSnakeCase(config.data)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
@ -119,6 +125,22 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
||||
useDeleteInterceptor(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
autoTransformBeforeSend(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
autoTransformBeforePost(): boolean {
|
||||
return this.autoTransformBeforeSend()
|
||||
}
|
||||
|
||||
autoTransformBeforePut(): boolean {
|
||||
return this.autoTransformBeforeSend()
|
||||
}
|
||||
|
||||
autoTransformBeforeDelete(): boolean {
|
||||
return this.autoTransformBeforeSend()
|
||||
}
|
||||
|
||||
/////////////////
|
||||
// Helper functions
|
||||
@ -370,6 +392,7 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
||||
const cancel = this.setLoading()
|
||||
|
||||
try {
|
||||
console.log('post', model.reactions)
|
||||
const response = await this.http.post(url, model)
|
||||
const result = this.modelUpdateFactory(response.data)
|
||||
if (typeof model.maxRight !== 'undefined') {
|
||||
|
32
frontend/src/services/reactions.ts
Normal file
32
frontend/src/services/reactions.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import AbstractService from '@/services/abstractService'
|
||||
import type {IAbstract} from '@/modelTypes/IAbstract'
|
||||
import ReactionModel from '@/models/reaction'
|
||||
import type {IReactionPerEntity} from '@/modelTypes/IReaction'
|
||||
import UserModel from '@/models/user'
|
||||
|
||||
export default class ReactionService extends AbstractService {
|
||||
constructor() {
|
||||
super({
|
||||
getAll: '{kind}/{id}/reactions',
|
||||
create: '{kind}/{id}/reactions',
|
||||
delete: '{kind}/{id}/reactions/delete',
|
||||
})
|
||||
}
|
||||
|
||||
modelFactory(data: Partial<IAbstract>): ReactionModel {
|
||||
return new ReactionModel(data)
|
||||
}
|
||||
|
||||
modelGetAllFactory(data: Partial<IReactionPerEntity>): Partial<IReactionPerEntity> {
|
||||
Object.keys(data).forEach(reaction => {
|
||||
data[reaction] = data[reaction]?.map(u => new UserModel(u))
|
||||
})
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
async delete(model: IAbstract) {
|
||||
const finalUrl = this.getReplacedRoute(this.paths.delete, model)
|
||||
return super.post(finalUrl, model)
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ import LabelService from './label'
|
||||
|
||||
import {colorFromHex} from '@/helpers/color/colorFromHex'
|
||||
import {SECONDS_A_DAY, SECONDS_A_HOUR, SECONDS_A_WEEK} from '@/constants/date'
|
||||
import {objectToSnakeCase} from '@/helpers/case'
|
||||
|
||||
const parseDate = date => {
|
||||
if (date) {
|
||||
@ -38,8 +39,12 @@ export default class TaskService extends AbstractService<ITask> {
|
||||
return this.processModel(model)
|
||||
}
|
||||
|
||||
autoTransformBeforePost(): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
processModel(updatedModel) {
|
||||
const model = { ...updatedModel }
|
||||
const model = {...updatedModel}
|
||||
|
||||
model.title = model.title?.trim()
|
||||
|
||||
@ -108,7 +113,15 @@ export default class TaskService extends AbstractService<ITask> {
|
||||
model.labels = model.labels.map(l => labelService.processModel(l))
|
||||
}
|
||||
|
||||
return model as ITask
|
||||
const transformed = objectToSnakeCase(model)
|
||||
|
||||
// We can't convert emojis to skane case, hence we add them back again
|
||||
transformed.reactions = {}
|
||||
Object.keys(updatedModel.reactions || {}).forEach(reaction => {
|
||||
transformed.reactions[reaction] = updatedModel.reactions[reaction].map(u => objectToSnakeCase(u))
|
||||
})
|
||||
|
||||
return transformed as ITask
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import AbstractService from './abstractService'
|
||||
import TaskCommentModel from '@/models/taskComment'
|
||||
import type {ITaskComment} from '@/modelTypes/ITaskComment'
|
||||
import {objectToSnakeCase} from '@/helpers/case'
|
||||
|
||||
export default class TaskCommentService extends AbstractService<ITaskComment> {
|
||||
constructor() {
|
||||
@ -16,4 +17,22 @@ export default class TaskCommentService extends AbstractService<ITaskComment> {
|
||||
modelFactory(data) {
|
||||
return new TaskCommentModel(data)
|
||||
}
|
||||
|
||||
autoTransformBeforePost(): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
beforeUpdate(model: ITaskComment) {
|
||||
const transformed = objectToSnakeCase({...model})
|
||||
|
||||
// We can't convert emojis to skane case, hence we add them back again
|
||||
transformed.reactions = {}
|
||||
Object.keys(model.reactions || {}).forEach(reaction => {
|
||||
transformed.reactions[reaction] = model.reactions[reaction].map(u => objectToSnakeCase(u))
|
||||
})
|
||||
|
||||
console.log()
|
||||
|
||||
return transformed as ITaskComment
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user