Basic discussions forum framework

Adds the basic structure for the Discussions MFE around which future development
will happen.
This commit is contained in:
Kshitij Sobti
2020-08-18 16:58:48 +05:30
parent 56d68e76ad
commit 491f7b7acd
58 changed files with 9313 additions and 4194 deletions

View File

@@ -0,0 +1,19 @@
import PropTypes from 'prop-types';
import React from 'react';
import Comment, { commentShape } from './comment/Comment';
function CommentsView({ comments }) {
return (
<div className="discussion-comments d-flex flex-column">
{
comments.map(comment => <Comment comment={comment} key={comment.id} />)
}
</div>
);
}
CommentsView.propTypes = {
comments: PropTypes.arrayOf(commentShape).isRequired,
};
export default CommentsView;

View File

@@ -0,0 +1,24 @@
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useParams } from 'react-router';
import CommentsView from './CommentsView';
import { selectTopicComments } from './data/selectors';
import { fetchTopicComments } from './data/thunks';
function CommentsViewContainer() {
const { threadId } = useParams();
const dispatch = useDispatch();
const comments = useSelector(selectTopicComments(threadId));
useEffect(() => {
// The courseId from the URL is the course we WANT to load.
dispatch(fetchTopicComments(threadId));
}, [threadId]);
return (
<CommentsView comments={comments} />
);
}
CommentsViewContainer.propTypes = {};
export default CommentsViewContainer;

View File

@@ -0,0 +1,56 @@
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import Button from '@edx/paragon/dist/Button';
import { faFlag } from '@fortawesome/free-regular-svg-icons';
import { faEllipsisV, faStar } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import PropTypes from 'prop-types';
import React from 'react';
import messages from './messages';
function Comment({ intl, comment }) {
return (
<div className="discussion-comment d-flex flex-column m-2 card">
<div className="header d-flex m-1 card-header">
<div className="avatar">
[A]
</div>
<div className="d-flex flex-column m-1">
<div className="title">
{/* TODO: Get title from thread */ }
Some title
</div>
<div className="status">
{/* TODO: get type from thread */ }
discussion posted about { comment.posted_on } by { comment.author }
</div>
</div>
<div className="d-flex icons m-1">
<FontAwesomeIcon icon={faStar} />
{ comment.abuse_flagged && <FontAwesomeIcon icon={faFlag} /> }
<FontAwesomeIcon icon={faEllipsisV} />
</div>
</div>
<div className="comment-body d-flex" dangerouslySetInnerHTML={{ __html: comment.rendered_body }} />
<div className="visibility-comment d-flex">
{/* TODO: Add parent group info */ }
</div>
<div className="actions d-flex">
<Button>{ intl.formatMessage(messages.add_response) }</Button>
</div>
</div>
);
}
export const commentShape = PropTypes.shape({
posted_on: PropTypes.string,
abuse_flagged: PropTypes.bool,
rendered_body: PropTypes.string,
author: PropTypes.string,
});
Comment.propTypes = {
intl: intlShape.isRequired,
comment: commentShape.isRequired,
};
export default injectIntl(Comment);

View File

@@ -0,0 +1,11 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
add_response: {
id: 'discussions.comments.comment.add-response',
defaultMessage: 'Add a response',
description: 'Button to add a response in a thread of forum posts',
},
});
export default messages;

View File

@@ -0,0 +1,31 @@
/* eslint-disable import/prefer-default-export */
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { API_BASE_URL } from '../../../data/constants';
export async function getThreadComments(
threadId, {
commentId, page, pageSize, requestedFields,
} = {},
) {
const url = new URL(`${API_BASE_URL}/api/discussion/v1/comments/`);
const paramsMap = {
thread_id: threadId,
comment_id: commentId,
page,
page_size: pageSize,
requested_fields: requestedFields,
};
Object.keys(paramsMap)
.forEach(
(param) => {
const paramValue = paramsMap[param];
if (paramValue) {
url.searchParams.append(param, paramValue);
}
},
);
const { data } = await getAuthenticatedHttpClient()
.get(url);
return data;
}

View File

@@ -0,0 +1 @@
export * from './slices';

View File

@@ -0,0 +1,4 @@
/* eslint-disable import/prefer-default-export */
export const selectTopicComments = topicId => state => state.comments.comments[topicId] || [];
export const courseTopicsStatus = state => state.comments.status;

View File

@@ -0,0 +1,43 @@
/* eslint-disable no-param-reassign,import/prefer-default-export */
import { createSlice } from '@reduxjs/toolkit';
import { LoadingStatus } from '../../../data/constants';
const commentsSlice = createSlice({
name: 'comments',
initialState: {
status: LoadingStatus.LOADING,
page: null,
comments: {
// Map thread ids to comments
},
totalPages: null,
totalThreads: null,
},
reducers: {
fetchCommentsRequest: (state) => {
state.status = LoadingStatus.LOADING;
},
fetchCommentsSuccess: (state, { payload }) => {
const { data, topicId } = payload;
state.status = LoadingStatus.LOADED;
state.comments[topicId] = data.results;
state.page = data.pagination.page;
state.totalPages = data.pagination.num_pages;
state.totalThreads = data.pagination.count;
},
fetchCommentsFailed: (state) => {
state.status = LoadingStatus.FAILED;
},
fetchCommentsDenied: (state) => {
state.status = LoadingStatus.DENIED;
},
},
});
export const {
fetchCommentsRequest,
fetchCommentsSuccess,
fetchCommentsFailed,
} = commentsSlice.actions;
export const commentsReducer = commentsSlice.reducer;

View File

@@ -0,0 +1,17 @@
/* eslint-disable import/prefer-default-export */
import { logError } from '@edx/frontend-platform/logging';
import { getThreadComments } from './api';
import { fetchCommentsFailed, fetchCommentsRequest, fetchCommentsSuccess } from './slices';
export function fetchTopicComments(topicId) {
return async (dispatch) => {
try {
dispatch(fetchCommentsRequest({ topicId }));
const data = await getThreadComments(topicId);
dispatch(fetchCommentsSuccess({ topicId, data }));
} catch (error) {
dispatch(fetchCommentsFailed());
logError(error);
}
};
}

View File

@@ -0,0 +1,2 @@
/* eslint-disable import/prefer-default-export */
export { default as CommentsViewContainer } from './CommentsViewContainer';