* feat: added draft functionality for comment and responses * fix: fixed comment update issue: * test: added draft test case * test: added mock conditions for tinymce * refactor: refactor code * test: added test cases * refactor: refactor hook file * refactor: fixed review issues * refactor: memoize function * refactor: refactor code * test: added update comment test case * refactor: refactor remove hook method * test: fixed test cases issue
326 lines
8.5 KiB
JavaScript
326 lines
8.5 KiB
JavaScript
import { useCallback, useContext, useMemo } from 'react';
|
|
|
|
import {
|
|
CheckCircle, CheckCircleOutline, Delete, Edit, InsertLink,
|
|
Institution, Lock, LockOpen, Pin, Report, School,
|
|
Verified, VerifiedOutline,
|
|
} from '@openedx/paragon/icons';
|
|
import { getIn } from 'formik';
|
|
import { uniqBy } from 'lodash';
|
|
import { useSelector } from 'react-redux';
|
|
import {
|
|
generatePath, matchPath, useLocation,
|
|
} from 'react-router-dom';
|
|
|
|
import { getConfig } from '@edx/frontend-platform';
|
|
|
|
import { DENIED, LOADED } from '../components/NavigationBar/data/slice';
|
|
import {
|
|
ContentActions, Routes, ThreadType,
|
|
} from '../data/constants';
|
|
import { ContentSelectors } from './data/constants';
|
|
import PostCommentsContext from './post-comments/postCommentsContext';
|
|
import messages from './messages';
|
|
|
|
/**
|
|
* Get HTTP Error status from generic error.
|
|
* @param error Generic caught error.
|
|
* @returns {number|null}
|
|
*/
|
|
export const getHttpErrorStatus = error => error?.customAttributes?.httpErrorStatus ?? error?.response?.status;
|
|
|
|
/**
|
|
* Return true if a field has been modified and is no longer valid
|
|
* @param {string} field Name of field
|
|
* @param {{}} errors formik error object
|
|
* @param {{}} touched formik touched object
|
|
* @returns {boolean}
|
|
*/
|
|
export function isFormikFieldInvalid(field, {
|
|
errors,
|
|
touched,
|
|
}) {
|
|
return Boolean(getIn(touched, field) && getIn(errors, field));
|
|
}
|
|
|
|
/**
|
|
* Hook to return the path for the current comments page
|
|
* @returns {string}
|
|
*/
|
|
export function useCommentsPagePath() {
|
|
const location = useLocation();
|
|
const { params: { page } } = matchPath({ path: Routes.COMMENTS.PAGE }, location.pathname);
|
|
return Routes.COMMENTS.PAGES[page];
|
|
}
|
|
|
|
/**
|
|
* Check if the provided comment or post supports the provided option.
|
|
* @param {{editableFields:[string]}} content
|
|
* @param {ContentActions} action
|
|
* @returns {boolean}
|
|
*/
|
|
export function checkPermissions(content, action) {
|
|
if (content.editableFields.includes(action)) {
|
|
return true;
|
|
}
|
|
// For delete action we check `content.canDelete`
|
|
if (action === ContentActions.DELETE) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* List of all possible actions for comments or posts.
|
|
*
|
|
* * `id` is a unique id for each action.
|
|
* * `action` is the action being performed. One action can
|
|
* have multiple mutually-exclusive entries (such as close/open)..
|
|
* * `icon` is the icon component to show for the action.
|
|
* * `label` is the translatable label message that can be passed to intl.
|
|
* * `conditions` is the an object where the key and value represent the key and value that should match
|
|
* in the content/post.
|
|
* e.g. for {pinned:false} the action will show up if the content/post has post.pinned==false
|
|
*/
|
|
export const ACTIONS_LIST = [
|
|
{
|
|
id: 'copy-link',
|
|
action: ContentActions.COPY_LINK,
|
|
icon: InsertLink,
|
|
label: messages.copyLink,
|
|
},
|
|
{
|
|
id: 'edit',
|
|
action: ContentActions.EDIT_CONTENT,
|
|
icon: Edit,
|
|
label: messages.editAction,
|
|
},
|
|
{
|
|
id: 'pin',
|
|
action: ContentActions.PIN,
|
|
icon: Pin,
|
|
label: messages.pinAction,
|
|
conditions: { pinned: false },
|
|
},
|
|
{
|
|
id: 'unpin',
|
|
action: ContentActions.PIN,
|
|
icon: Pin,
|
|
label: messages.unpinAction,
|
|
conditions: { pinned: true },
|
|
},
|
|
{
|
|
id: 'endorse',
|
|
action: ContentActions.ENDORSE,
|
|
icon: VerifiedOutline,
|
|
label: messages.endorseAction,
|
|
conditions: {
|
|
endorsed: false,
|
|
postType: ThreadType.DISCUSSION,
|
|
},
|
|
},
|
|
{
|
|
id: 'unendorse',
|
|
action: ContentActions.ENDORSE,
|
|
icon: Verified,
|
|
label: messages.unendorseAction,
|
|
conditions: {
|
|
endorsed: true,
|
|
postType: ThreadType.DISCUSSION,
|
|
},
|
|
},
|
|
{
|
|
id: 'answer',
|
|
action: ContentActions.ENDORSE,
|
|
icon: CheckCircleOutline,
|
|
label: messages.markAnsweredAction,
|
|
conditions: {
|
|
endorsed: false,
|
|
postType: ThreadType.QUESTION,
|
|
},
|
|
},
|
|
{
|
|
id: 'unanswer',
|
|
action: ContentActions.ENDORSE,
|
|
icon: CheckCircle,
|
|
label: messages.unmarkAnsweredAction,
|
|
conditions: {
|
|
endorsed: true,
|
|
postType: ThreadType.QUESTION,
|
|
},
|
|
},
|
|
{
|
|
id: 'close',
|
|
action: ContentActions.CLOSE,
|
|
icon: Lock,
|
|
label: messages.closeAction,
|
|
conditions: { closed: false },
|
|
},
|
|
{
|
|
id: 'reopen',
|
|
action: ContentActions.CLOSE,
|
|
icon: LockOpen,
|
|
label: messages.reopenAction,
|
|
conditions: { closed: true },
|
|
},
|
|
{
|
|
id: 'report',
|
|
action: ContentActions.REPORT,
|
|
icon: Report,
|
|
label: messages.reportAction,
|
|
conditions: { abuseFlagged: false },
|
|
},
|
|
{
|
|
id: 'unreport',
|
|
action: ContentActions.REPORT,
|
|
icon: Report,
|
|
label: messages.unreportAction,
|
|
conditions: { abuseFlagged: true },
|
|
},
|
|
{
|
|
id: 'delete',
|
|
action: ContentActions.DELETE,
|
|
icon: Delete,
|
|
label: messages.deleteAction,
|
|
conditions: { canDelete: true },
|
|
},
|
|
];
|
|
|
|
export function useActions(contentType, id) {
|
|
const { postType } = useContext(PostCommentsContext);
|
|
const content = { ...useSelector(ContentSelectors[contentType](id)), postType };
|
|
|
|
const checkConditions = useCallback((item, conditions) => (
|
|
conditions
|
|
? Object.keys(conditions)
|
|
.map(key => item[key] === conditions[key])
|
|
.every(condition => condition === true)
|
|
: true
|
|
), []);
|
|
|
|
const actions = useMemo(() => ACTIONS_LIST.filter(
|
|
({
|
|
action,
|
|
conditions = null,
|
|
}) => checkPermissions(content, action) && checkConditions(content, conditions),
|
|
), [content]);
|
|
|
|
return actions;
|
|
}
|
|
|
|
export const formikCompatibleHandler = (formikHandler, name) => (value) => formikHandler({
|
|
target: {
|
|
name,
|
|
value,
|
|
},
|
|
});
|
|
|
|
/**
|
|
* A wrapper for the generatePath function that generates a new path that keeps the existing
|
|
* query parameters intact
|
|
* @param path
|
|
* @param params
|
|
* @return {function(*): *&{pathname: *}}
|
|
*/
|
|
export const discussionsPath = (path, params) => {
|
|
const pathname = generatePath(path, params);
|
|
return (location) => ({ ...location, pathname });
|
|
};
|
|
|
|
/**
|
|
* Helper function to make a postMessage call
|
|
* @param {string} type message type
|
|
* @param {object} payload data to send in message
|
|
*/
|
|
export function postMessageToParent(type, payload = {}) {
|
|
if (window.parent !== window) {
|
|
const messageTargets = [
|
|
getConfig().LEARNING_BASE_URL,
|
|
getConfig().LMS_BASE_URL,
|
|
];
|
|
messageTargets.forEach(target => {
|
|
window.parent.postMessage(
|
|
{
|
|
type,
|
|
payload,
|
|
},
|
|
target,
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
export const isPostPreviewAvailable = (htmlNode) => {
|
|
const containsImage = htmlNode.match(/(<img((?:\\.|.)*)>)/);
|
|
const isLatex = htmlNode.match(/(\${1,2})((?:\\.|.)*)/)
|
|
|| htmlNode.match(/(\[mathjax](.+?))+/)
|
|
|| htmlNode.match(/(\[mathjaxinline](.+?))+/)
|
|
|| htmlNode.match(/(\\\[(.+?))+/)
|
|
|| htmlNode.match(/(\\\((.+?))+/);
|
|
|
|
if (containsImage || isLatex || htmlNode === '') {
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Helper function to filter posts
|
|
* @param {array} posts arrays of posts
|
|
* @param {string} filterBy name of post object attribute. un will use for reverse
|
|
* condition. like pinned attribute for pinned post and unpinned for non pinned posts.
|
|
*/
|
|
export const filterPosts = (posts, filterBy) => uniqBy(posts, 'id').filter(
|
|
post => (filterBy.startsWith('un') ? !post[filterBy.slice(2)] : post[filterBy]),
|
|
);
|
|
|
|
export function handleKeyDown(event) {
|
|
const { key } = event;
|
|
if (key !== 'ArrowDown' && key !== 'ArrowUp') { return; }
|
|
const option = event.target;
|
|
|
|
let selectedOption;
|
|
if (key === 'ArrowDown') { selectedOption = option.nextElementSibling; }
|
|
if (key === 'ArrowUp') { selectedOption = option.previousElementSibling; }
|
|
|
|
if (selectedOption) {
|
|
selectedOption.focus();
|
|
}
|
|
}
|
|
|
|
export function isLastElementOfList(list, element) {
|
|
return list[list.length - 1] === element;
|
|
}
|
|
|
|
export function truncatePath(path) {
|
|
return path.substring(0, path.lastIndexOf('/'));
|
|
}
|
|
|
|
export function getAuthorLabel(intl, authorLabel) {
|
|
const authorLabelMappings = {
|
|
Staff: {
|
|
icon: Institution,
|
|
authorLabelMessage: intl.formatMessage(messages.authorLabelStaff),
|
|
},
|
|
Moderator: {
|
|
icon: School,
|
|
authorLabelMessage: intl.formatMessage(messages.authorLabelModerator),
|
|
},
|
|
'Community TA': {
|
|
icon: School,
|
|
authorLabelMessage: intl.formatMessage(messages.authorLabelTA),
|
|
},
|
|
};
|
|
|
|
return authorLabelMappings[authorLabel] || {};
|
|
}
|
|
|
|
export const isCourseStatusValid = (courseStatus) => [DENIED, LOADED].includes(courseStatus);
|
|
|
|
export const extractContent = (content) => {
|
|
if (typeof content === 'object') {
|
|
return content.target.getContent();
|
|
}
|
|
return content;
|
|
};
|