diff --git a/backend/controller/comments.controller.js b/backend/controller/comments.controller.js new file mode 100644 index 0000000..c707b3a --- /dev/null +++ b/backend/controller/comments.controller.js @@ -0,0 +1,28 @@ +const { createComment, getAllComments } = require('../service/comments.service') + +const getAllCommentsController = (req, res) => { + const ticketID = req.params.id + getAllComments(ticketID) + .then((data) => res.send(data)) + .catch((err) => { + res.status(500).send(err) + console.log(err) + }) +} + +const createCommentController = (req, res) => { + createComment(req.body) + .then((data) => { + console.log(data) + res.send(data) + }) + .catch((error) => { + console.log(error) + res.status(500).send(error) + }) +} + +module.exports = { + createCommentController, + getAllCommentsController, +} diff --git a/backend/models/comment.model.js b/backend/models/comment.model.js index 5ee409f..a08431d 100644 --- a/backend/models/comment.model.js +++ b/backend/models/comment.model.js @@ -1,11 +1,14 @@ const mongoose = require('mongoose') const { Schema } = mongoose +const Mixed = Schema.Types.Mixed + const CommentSchema = new Schema( { - // reference_code would be parent ticket here - reference_code: { type: String, index: true }, - commment: { type: String }, + // reference_code would be parent ticket/parent comment here + reference_code: { type: String, index: true, required: true }, + comment: { type: [Mixed], required: true }, + author_id: { type: String, required: true }, }, { timestamps: true, diff --git a/backend/routes/comments.routes.js b/backend/routes/comments.routes.js new file mode 100644 index 0000000..e2c1bbd --- /dev/null +++ b/backend/routes/comments.routes.js @@ -0,0 +1,14 @@ +const router = require('express').Router() +const { authenticateUser } = require('../auth/middleware') +const { + createCommentController, + getAllCommentsController, +} = require('../controller/comments.controller') + +router.route('/:id').get(authenticateUser, getAllCommentsController) + +router.route('/').post(authenticateUser, createCommentController) + +//TODO: delete & edit + +module.exports = router diff --git a/backend/server.js b/backend/server.js index fe56817..67c4b78 100644 --- a/backend/server.js +++ b/backend/server.js @@ -31,6 +31,7 @@ const UWFinancePurchaseRouter = require('./routes/uwfinancepurchases.routes') const usersRouter = require('./routes/users.routes') const groupRouter = require('./routes/googlegroup.routes') const filesRouter = require('./routes/files.routes') +const commentRouter = require('./routes/comments.routes') app.use(express.json()) app.use('/fundingitems', fundingItemsRouter) @@ -40,6 +41,7 @@ app.use('/uwfinancepurchases', UWFinancePurchaseRouter) app.use('/users', usersRouter) app.use('/googlegroups', groupRouter) app.use('/files', filesRouter) +app.use('/comments', commentRouter) app.listen(port, async () => { console.log(`Server is running on port: ${port}`) diff --git a/backend/service/comments.service.js b/backend/service/comments.service.js new file mode 100644 index 0000000..f1e5d75 --- /dev/null +++ b/backend/service/comments.service.js @@ -0,0 +1,47 @@ +const Comment = require('../models/comment.model') + +const createComment = async (body) => { + const comment = new Comment(body) + const newComment = await comment.save() + return newComment +} + +const getAllComments = async (code) => { + //add reply aggregation, sortby + const res = await Comment.aggregate([ + { + $match: { + reference_code: code, + }, + }, + { + $lookup: { + from: 'comments', + let: { idStr: { $toString: '$_id' } }, // Define variable for use in the pipeline + pipeline: [ + { + $match: { + $expr: { $eq: ['$reference_code', '$$idStr'] }, // Use the variable to match documents + }, + }, + { $sort: { createdAt: 1 } }, // Sort matching documents in ascending order + ], + as: 'replies', + }, + }, + { + $sort: { createdAt: -1 }, + }, + { + $set: { + replies: '$replies', + }, + }, + ]) + return res +} + +module.exports = { + createComment, + getAllComments, +} diff --git a/frontend/src/components/CommentInput.js b/frontend/src/components/CommentInput.js new file mode 100644 index 0000000..744a816 --- /dev/null +++ b/frontend/src/components/CommentInput.js @@ -0,0 +1,167 @@ +import { useCallback, useMemo, useState } from 'react' +import isHotkey from 'is-hotkey' +import { Editable, withReact, Slate } from 'slate-react' +import { createEditor } from 'slate' +import { withHistory } from 'slate-history' +import { useAuth } from '../contexts/AuthContext' +import { axiosPreset } from '../axiosConfig' +import { Box, Button } from '@chakra-ui/react' + +import { + BlockButton, + Element, + Leaf, + MarkButton, + Toolbar, + toggleMark, +} from './SlateComponents' + +const HOTKEYS = { + 'mod+b': 'bold', + 'mod+i': 'italic', + 'mod+u': 'underline', +} + +//Cleans leading and ending white space +const cleanInput = (val) => { + const textList = val.map((item) => item['children'][0]['text']) + + const firstIndex = textList.findIndex((text) => text != '') + if (firstIndex == -1) { + return [] + } + + const lastIndex = textList.findLastIndex((text) => text != '') + + return val.slice(firstIndex, lastIndex + 1) +} + +//Disables the "send" button if input isn't valid +const invalidInput = (val) => { + for (let i = 0; i < val.length; i++) { + if (val[i]['children'][0]['text'] != '') { + return false + } + } + return true +} + +const CommentInput = ({ code, getComments, reply, onClose, ticket }) => { + const renderElement = useCallback((props) => , []) + const renderLeaf = useCallback((props) => , []) + const editor = useMemo(() => withHistory(withReact(createEditor())), []) + const [loading, setLoading] = useState(false) + const auth = useAuth() + const [val, setVal] = useState(editor.children) + const handleSubmit = (ref, ticket) => { + setLoading(true) + const comment = cleanInput(val) + if (comment.length === 0) { + return + } + const payload = { + author_id: auth.currentUser.uid, + comment: comment, + reference_code: ref, + } + axiosPreset + .post('/comments', payload) + .then(() => getComments(ticket).then(() => setLoading(false))) + + .catch(() => setLoading(false)) + } + + return ( + { + setVal([...editor.children]) + }} + > + { + for (const hotkey in HOTKEYS) { + if (isHotkey(hotkey, event)) { + event.preventDefault() + const mark = HOTKEYS[hotkey] + toggleMark(editor, mark) + } + } + }} + /> + + + + + + + + + + + {reply && ( + + )} + + + + + ) +} + +//TODO: If replying another reply, make the initial value quote the comment above (makes it easier to keep track who's replying to who) +const initialValue = [ + { + type: 'paragraph', + children: [{ text: '' }], + }, +] + +export default CommentInput diff --git a/frontend/src/components/CommentSection.js b/frontend/src/components/CommentSection.js index 4ad4600..9d6aa35 100644 --- a/frontend/src/components/CommentSection.js +++ b/frontend/src/components/CommentSection.js @@ -1,114 +1,89 @@ // modified version of https://github.com/ianstormtaylor/slate/blob/main/site/examples/richtext.tsx -import React, { useCallback, useMemo } from 'react' -import isHotkey from 'is-hotkey' -import { Editable, withReact, Slate } from 'slate-react' -import { createEditor } from 'slate' -import { withHistory } from 'slate-history' +import React, { useState, useEffect } from 'react' +import { Box, Spinner } from '@chakra-ui/react' +import { axiosPreset } from '../axiosConfig' +import CommentInput from './CommentInput' +import CommentView from './CommentView' -import { - BlockButton, - Element, - Leaf, - MarkButton, - Toolbar, - toggleMark, -} from './SlateComponents' -import { Box, Heading } from '@chakra-ui/react' +const CommentSection = ({ ticket, allUsers }) => { + const [comments, setComments] = useState([]) + const [refreshKey, setRefreshKey] = useState(0) + const [loading, setLoading] = useState(false) -const HOTKEYS = { - 'mod+b': 'bold', - 'mod+i': 'italic', - 'mod+u': 'underline', - 'mod+`': 'code', -} + const forceRefresh = () => { + setRefreshKey((oldKey) => oldKey + 1) + } + + const getComments = async (ref) => { + setLoading(true) + axiosPreset + .get('/comments/' + ref) + .then((data) => { + setComments([...data.data]) + forceRefresh() + console.log(data) + }) + .then(() => { + setLoading(false) + }) + .catch(() => { + setLoading(false) + }) + } -const CommentSection = () => { - const renderElement = useCallback((props) => , []) - const renderLeaf = useCallback((props) => , []) - const editor = useMemo(() => withHistory(withReact(createEditor())), []) + useEffect(() => { + getComments(ticket) + }, [ticket]) return ( - - Comment Section - - - - - - - - - - - - - - - - - { - for (const hotkey in HOTKEYS) { - if (isHotkey(hotkey, event)) { - event.preventDefault() - const mark = HOTKEYS[hotkey] - toggleMark(editor, mark) - } - } - }} + + + Comments + +
+ - + + {loading && ( + + )} + + {comments.map((content, index) => { + return ( + + ) + })} + + +
) } -// example taken from https://github.com/ianstormtaylor/slate/blob/main/site/components.tsx -// remove once dynamically fetched from backend -const initialValue = [ - { - type: 'paragraph', - children: [ - { text: 'This is editable ' }, - { text: 'rich', bold: true }, - { text: ' text, ' }, - { text: 'much', italic: true }, - { text: ' better than a ' }, - { text: '