Skip to content
Snippets Groups Projects
Commit 4d0de580 authored by Christos's avatar Christos
Browse files

Merge branch 'comments-title-feature' into 'master'

Comments title feature

See merge request !515
parents e06c43d2 8029ca69
No related branches found
No related tags found
1 merge request!515Comments title feature
Pipeline #55907 passed with stages
in 3 minutes and 17 seconds
Showing with 174 additions and 65 deletions
...@@ -178,6 +178,9 @@ export default { ...@@ -178,6 +178,9 @@ export default {
), ),
], ],
ImageService: { showAlt: true }, ImageService: { showAlt: true },
CommentsService: {
showTitle: true
},
CustomTagService: { CustomTagService: {
tags: [ tags: [
{ label: 'custom-tag-label-1', tagType: 'inline' }, { label: 'custom-tag-label-1', tagType: 'inline' },
......
...@@ -147,6 +147,7 @@ const en = { ...@@ -147,6 +147,7 @@ const en = {
Cancel: 'Cancel', Cancel: 'Cancel',
Reply: 'Reply', Reply: 'Reply',
'Write comment': 'Write comment', 'Write comment': 'Write comment',
'Write title': 'Write title',
}, },
Various: { Various: {
Add: 'Add', Add: 'Add',
......
...@@ -148,6 +148,7 @@ const es = { ...@@ -148,6 +148,7 @@ const es = {
Cancel: 'Cancelar', Cancel: 'Cancelar',
Reply: 'Responder', Reply: 'Responder',
'Write comment': 'Escribir comentario', 'Write comment': 'Escribir comentario',
'Write title': 'Escribir titulo',
}, },
Various: { Various: {
Add: 'Agregar', Add: 'Agregar',
......
...@@ -31,10 +31,12 @@ export default class CommentsService extends Service { ...@@ -31,10 +31,12 @@ export default class CommentsService extends Service {
} }
register() { register() {
const commentConfig = this.config.get('config.CommentsService');
const createMark = this.container.get('CreateMark'); const createMark = this.container.get('CreateMark');
createMark( createMark(
{ {
comment: commentMark, comment: commentMark(commentConfig?.showTitle || false),
}, },
{ toWaxSchema: true }, { toWaxSchema: true },
); );
......
/* eslint-disable no-param-reassign */
/* eslint react/prop-types: 0 */ /* eslint react/prop-types: 0 */
import React, { useContext, useMemo, useState, useEffect } from 'react'; import React, { useContext, useMemo, useState, useEffect } from 'react';
import { TextSelection } from 'prosemirror-state'; import { TextSelection } from 'prosemirror-state';
...@@ -55,7 +56,10 @@ export default ({ comment, top, commentId, recalculateTops }) => { ...@@ -55,7 +56,10 @@ export default ({ comment, top, commentId, recalculateTops }) => {
}; };
const commentConfig = app.config.get('config.CommentsService'); const commentConfig = app.config.get('config.CommentsService');
const isReadOnly = commentConfig ? commentConfig.readOnly : false; const isReadOnly =
commentConfig && commentConfig.readOnly ? commentConfig.readOnly : false;
const showTitle =
commentConfig && commentConfig.showTitle ? commentConfig.showTitle : false;
const commentPlugin = app.PmPlugins.get('commentPlugin'); const commentPlugin = app.PmPlugins.get('commentPlugin');
const activeComment = commentPlugin.getState(activeView.state).comment; const activeComment = commentPlugin.getState(activeView.state).comment;
...@@ -68,16 +72,17 @@ export default ({ comment, top, commentId, recalculateTops }) => { ...@@ -68,16 +72,17 @@ export default ({ comment, top, commentId, recalculateTops }) => {
} }
}, [activeComment]); }, [activeComment]);
const onClickPost = content => { const onClickPost = ({ commentValue, title }) => {
const { tr } = state;
setClickPost(true); setClickPost(true);
const obj = { const obj = {
content, content: commentValue,
displayName: user.username, displayName: user.username,
timestamp: Math.floor(Date.now()), timestamp: Math.floor(Date.now()),
}; };
comment.attrs.title = title || comment.attrs.title;
comment.attrs.conversation.push(obj); comment.attrs.conversation.push(obj);
const id = uuidv4(); const id = uuidv4();
allCommentsWithSameId.forEach(singleComment => { allCommentsWithSameId.forEach(singleComment => {
activeView.dispatch( activeView.dispatch(
...@@ -98,6 +103,7 @@ export default ({ comment, top, commentId, recalculateTops }) => { ...@@ -98,6 +103,7 @@ export default ({ comment, top, commentId, recalculateTops }) => {
group: comment.attrs.group, group: comment.attrs.group,
viewid: comment.attrs.viewid, viewid: comment.attrs.viewid,
conversation: comment.attrs.conversation, conversation: comment.attrs.conversation,
title: comment.attrs.title,
}), }),
) )
.setMeta('forceUpdate', true), .setMeta('forceUpdate', true),
...@@ -148,17 +154,13 @@ export default ({ comment, top, commentId, recalculateTops }) => { ...@@ -148,17 +154,13 @@ export default ({ comment, top, commentId, recalculateTops }) => {
activeView.focus(); activeView.focus();
}; };
const onTextAreaBlur = (content, isNewComment) => { const onTextAreaBlur = () => {
// TODO Save into local storage // TODO Save into local storage
// if (content !== '') { // if (content !== '') {
// onClickPost(content); // onClickPost(content);
// } // }
setTimeout(() => { setTimeout(() => {
if ( if (comment.attrs.conversation.length === 0 && !clickPost) {
comment.attrs.conversation.length === 0 &&
isNewComment &&
!clickPost
) {
onClickResolve(); onClickResolve();
activeView.focus(); activeView.focus();
} }
...@@ -184,6 +186,8 @@ export default ({ comment, top, commentId, recalculateTops }) => { ...@@ -184,6 +186,8 @@ export default ({ comment, top, commentId, recalculateTops }) => {
onClickResolve={onClickResolve} onClickResolve={onClickResolve}
onTextAreaBlur={onTextAreaBlur} onTextAreaBlur={onTextAreaBlur}
recalculateTops={recalculateTops} recalculateTops={recalculateTops}
showTitle={showTitle}
title={comment.attrs.title}
/> />
</ConnectedCommentStyled> </ConnectedCommentStyled>
), ),
......
...@@ -81,6 +81,8 @@ const CommentBox = props => { ...@@ -81,6 +81,8 @@ const CommentBox = props => {
onClickPost, onClickPost,
onClickResolve, onClickResolve,
onTextAreaBlur, onTextAreaBlur,
title,
showTitle,
} = props; } = props;
// send signal to make this comment active // send signal to make this comment active
...@@ -91,7 +93,6 @@ const CommentBox = props => { ...@@ -91,7 +93,6 @@ const CommentBox = props => {
if (!active && (!commentData || commentData.length === 0)) return null; if (!active && (!commentData || commentData.length === 0)) return null;
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
return ( return (
<Wrapper active={active} className={className} onClick={onClickWrapper}> <Wrapper active={active} className={className} onClick={onClickWrapper}>
{active && commentData.length > 0 && ( {active && commentData.length > 0 && (
...@@ -109,15 +110,14 @@ const CommentBox = props => { ...@@ -109,15 +110,14 @@ const CommentBox = props => {
</Resolve> </Resolve>
</Head> </Head>
)} )}
<CommentItemList active={active} data={commentData} title={title} />
<CommentItemList active={active} data={commentData} />
{active && ( {active && (
<StyledReply <StyledReply
isNewComment={commentData.length === 0} isNewComment={commentData.length === 0}
isReadOnly={isReadOnly} isReadOnly={isReadOnly}
onClickPost={onClickPost} onClickPost={onClickPost}
onTextAreaBlur={onTextAreaBlur} onTextAreaBlur={onTextAreaBlur}
showTitle={showTitle}
/> />
)} )}
</Wrapper> </Wrapper>
...@@ -150,11 +150,14 @@ CommentBox.propTypes = { ...@@ -150,11 +150,14 @@ CommentBox.propTypes = {
onClickResolve: PropTypes.func.isRequired, onClickResolve: PropTypes.func.isRequired,
/** Function to run when text area loses focus */ /** Function to run when text area loses focus */
onTextAreaBlur: PropTypes.func.isRequired, onTextAreaBlur: PropTypes.func.isRequired,
title: PropTypes.string,
showTitle: PropTypes.bool.isRequired,
}; };
CommentBox.defaultProps = { CommentBox.defaultProps = {
active: false, active: false,
commentData: [], commentData: [],
title: null,
}; };
export default CommentBox; export default CommentBox;
...@@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react'; ...@@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import styled from 'styled-components'; import styled from 'styled-components';
import { clone, uniqueId } from 'lodash'; import { clone, uniqueId } from 'lodash';
import { override } from '@pubsweet/ui-toolkit'; import { override, th } from '@pubsweet/ui-toolkit';
import CommentItem from './CommentItem'; import CommentItem from './CommentItem';
...@@ -15,6 +15,13 @@ const Wrapper = styled.div` ...@@ -15,6 +15,13 @@ const Wrapper = styled.div`
${override('Wax.CommentItemWrapper')} ${override('Wax.CommentItemWrapper')}
`; `;
const CommentTitle = styled.span`
font-weight: bold;
font-size: ${th('fontSizeBase')};
${override('Wax.CommentItemTitle')}
`;
const More = styled.span` const More = styled.span`
background: gray; background: gray;
border-radius: 3px; border-radius: 3px;
...@@ -27,7 +34,7 @@ const More = styled.span` ...@@ -27,7 +34,7 @@ const More = styled.span`
`; `;
const CommentItemList = props => { const CommentItemList = props => {
const { active, className, data } = props; const { active, className, data, title } = props;
if (!data || data.length === 0) return null; if (!data || data.length === 0) return null;
const [items, setItems] = useState(data); const [items, setItems] = useState(data);
...@@ -49,6 +56,7 @@ const CommentItemList = props => { ...@@ -49,6 +56,7 @@ const CommentItemList = props => {
return ( return (
<Wrapper active={active} className={className}> <Wrapper active={active} className={className}>
{title && <CommentTitle>{title}</CommentTitle>}
{items.map(item => ( {items.map(item => (
<CommentItem <CommentItem
active={active} active={active}
...@@ -79,11 +87,13 @@ CommentItemList.propTypes = { ...@@ -79,11 +87,13 @@ CommentItemList.propTypes = {
timestamp: PropTypes.number.isRequired, timestamp: PropTypes.number.isRequired,
}), }),
), ),
title: PropTypes.string,
}; };
CommentItemList.defaultProps = { CommentItemList.defaultProps = {
active: false, active: false,
data: [], data: [],
title: null,
}; };
export default CommentItemList; export default CommentItemList;
...@@ -4,6 +4,7 @@ import styled, { css } from 'styled-components'; ...@@ -4,6 +4,7 @@ import styled, { css } from 'styled-components';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { grid, th, override } from '@pubsweet/ui-toolkit'; import { grid, th, override } from '@pubsweet/ui-toolkit';
import { useOnClickOutside } from 'wax-prosemirror-core';
const Wrapper = styled.div` const Wrapper = styled.div`
background: ${th('colorBackgroundHue')}; background: ${th('colorBackgroundHue')};
...@@ -14,6 +15,21 @@ const Wrapper = styled.div` ...@@ -14,6 +15,21 @@ const Wrapper = styled.div`
const TextWrapper = styled.div``; const TextWrapper = styled.div``;
const CommentTitle = styled.input`
background: ${th('colorBackgroundHue')};
border: 3px solid ${th('colorBackgroundTabs')};
font-family: ${th('fontWriting')};
margin-bottom: 10px;
position: relative;
width: 100%;
&:focus {
outline: 1px solid ${th('colorPrimary')};
}
${override('Wax.CommentTitle')}
`;
const ReplyTextArea = styled.textarea` const ReplyTextArea = styled.textarea`
background: ${th('colorBackgroundHue')}; background: ${th('colorBackgroundHue')};
border: 3px solid ${th('colorBackgroundTabs')}; border: 3px solid ${th('colorBackgroundTabs')};
...@@ -66,40 +82,61 @@ const CommentReply = props => { ...@@ -66,40 +82,61 @@ const CommentReply = props => {
onClickPost, onClickPost,
isReadOnly, isReadOnly,
onTextAreaBlur, onTextAreaBlur,
showTitle,
} = props; } = props;
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const commentInput = useRef(null); const commentInput = useRef(null);
const commentTitle = useRef(null);
const [commentValue, setCommentValue] = useState(''); const [commentValue, setCommentValue] = useState('');
const [title, setTitle] = useState('');
const ref = useRef(null);
useOnClickOutside(ref, onTextAreaBlur);
useEffect(() => { useEffect(() => {
setTimeout(() => { setTimeout(() => {
if (commentInput.current && isNewComment) commentInput.current.focus(); if (commentTitle.current && isNewComment) commentTitle.current.focus();
if (commentInput.current && !isNewComment) commentInput.current.focus();
}); });
}, []); }, []);
const handleSubmit = e => { const handleSubmit = e => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
onClickPost(commentValue); onClickPost({ title, commentValue });
setCommentValue(''); setCommentValue('');
setTitle('');
}; };
const resetValue = e => { const resetValue = e => {
e.preventDefault(); e.preventDefault();
setCommentValue(''); setCommentValue('');
}; setTitle('');
const onBlur = content => {
onTextAreaBlur(content, isNewComment);
}; };
return ( return (
<Wrapper className={className}> <Wrapper className={className} ref={ref}>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<TextWrapper> <TextWrapper>
{isNewComment && showTitle && (
<CommentTitle
name="title"
onChange={e => {
setTitle(e.target.value);
}}
placeholder={`${
!isEmpty(i18n) && i18n.exists(`Wax.Comments.Write title`)
? t(`Wax.Comments.Write title`)
: 'Write title'
}...`}
ref={commentTitle}
type="text"
value={title}
/>
)}
<ReplyTextArea <ReplyTextArea
cols="5" cols="5"
onBlur={() => onBlur(commentInput.current.value)}
onChange={() => setCommentValue(commentInput.current.value)} onChange={() => setCommentValue(commentInput.current.value)}
onKeyDown={e => { onKeyDown={e => {
if (e.keyCode === 13 && !e.shiftKey) { if (e.keyCode === 13 && !e.shiftKey) {
...@@ -155,6 +192,7 @@ CommentReply.propTypes = { ...@@ -155,6 +192,7 @@ CommentReply.propTypes = {
onClickPost: PropTypes.func.isRequired, onClickPost: PropTypes.func.isRequired,
isReadOnly: PropTypes.bool.isRequired, isReadOnly: PropTypes.bool.isRequired,
onTextAreaBlur: PropTypes.func.isRequired, onTextAreaBlur: PropTypes.func.isRequired,
showTitle: PropTypes.bool.isRequired,
}; };
CommentReply.defaultProps = {}; CommentReply.defaultProps = {};
......
const commentMark = { /* eslint-disable no-param-reassign */
attrs: { const commentMark = showTitle => {
class: { default: 'comment' }, const comment = {
id: { default: '' }, attrs: {
group: { default: '' }, class: { default: 'comment' },
viewid: { default: '' }, id: { default: '' },
conversation: [], group: { default: '' },
}, viewid: { default: '' },
inclusive: false, conversation: [],
excludes: '',
parseDOM: [
{
tag: 'span.comment',
getAttrs(hook, next) {
Object.assign(hook, {
class: hook.dom.getAttribute('class'),
id: hook.dom.dataset.id,
group: hook.dom.dataset.group,
viewid: hook.dom.dataset.viewid,
conversation: JSON.parse(hook.dom.dataset.conversation),
});
next();
},
}, },
], inclusive: false,
toDOM(hook, next) { excludes: '',
hook.value = [ parseDOM: [
'span',
{ {
class: hook.node.attrs.class, tag: 'span.comment',
'data-id': hook.node.attrs.id, getAttrs(hook, next) {
'data-conversation': JSON.stringify(hook.node.attrs.conversation), const parsedDom = {
'data-viewid': hook.node.attrs.viewid, class: hook.dom.getAttribute('class'),
'data-group': hook.node.attrs.group, id: hook.dom.dataset.id,
group: hook.dom.dataset.group,
viewid: hook.dom.dataset.viewid,
conversation: JSON.parse(hook.dom.dataset.conversation),
};
if (showTitle) {
parsedDom.title = hook.dom.dataset.title;
}
Object.assign(hook, parsedDom);
next();
},
}, },
]; ],
next(); toDOM(hook, next) {
}, hook.value = [
'span',
{
class: hook.node.attrs.class,
'data-id': hook.node.attrs.id,
'data-conversation': JSON.stringify(hook.node.attrs.conversation),
'data-viewid': hook.node.attrs.viewid,
'data-group': hook.node.attrs.group,
},
];
if (showTitle) {
hook.value[1]['data-title'] = hook.node.attrs.title;
}
next();
},
};
if (showTitle) {
comment.attrs.title = { default: '' };
}
return comment;
}; };
export default commentMark; export default commentMark;
/* eslint-disable no-console */
import { Service } from 'wax-prosemirror-core'; import { Service } from 'wax-prosemirror-core';
import { yCursorPlugin, ySyncPlugin, yUndoPlugin } from 'y-prosemirror'; import { yCursorPlugin, ySyncPlugin, yUndoPlugin } from 'y-prosemirror';
import { WebsocketProvider } from 'y-websocket'; import { WebsocketProvider } from 'y-websocket';
...@@ -7,10 +8,22 @@ import './yjs.css'; ...@@ -7,10 +8,22 @@ import './yjs.css';
class YjsService extends Service { class YjsService extends Service {
name = 'YjsService'; name = 'YjsService';
boot() { boot() {
const { connectionUrl, docIdentifier } = this.config; const {
const ydoc = new Y.Doc(); connectionUrl,
// const provider = new WebsocketProvider('wss://demos.yjs.dev', 'prosemirror-demo', ydoc) docIdentifier,
const provider = new WebsocketProvider(connectionUrl, docIdentifier, ydoc); cursorBuilder,
provider: configProvider,
ydoc: configYdoc,
} = this.config;
let provider = configProvider ? configProvider() : null;
let ydoc = configYdoc ? configYdoc() : null;
if (!configProvider || !configYdoc) {
ydoc = new Y.Doc();
provider = new WebsocketProvider(connectionUrl, docIdentifier, ydoc);
}
provider.on('sync', args => { provider.on('sync', args => {
console.log({ sync: args }); console.log({ sync: args });
}); });
...@@ -23,9 +36,23 @@ class YjsService extends Service { ...@@ -23,9 +36,23 @@ class YjsService extends Service {
provider.on('connection-error', args => { provider.on('connection-error', args => {
console.log({ connectioError: args }); console.log({ connectioError: args });
}); });
const type = ydoc.getXmlFragment('prosemirror'); const type = ydoc.getXmlFragment('prosemirror');
this.app.PmPlugins.add('ySyncPlugin', ySyncPlugin(type)); this.app.PmPlugins.add('ySyncPlugin', ySyncPlugin(type));
this.app.PmPlugins.add('yCursorPlugin', yCursorPlugin(provider.awareness));
if (cursorBuilder) {
this.app.PmPlugins.add(
'yCursorPlugin',
yCursorPlugin(provider.awareness, { cursorBuilder }),
);
} else {
this.app.PmPlugins.add(
'yCursorPlugin',
yCursorPlugin(provider.awareness),
);
}
this.app.PmPlugins.add('yUndoPlugin', yUndoPlugin()); this.app.PmPlugins.add('yUndoPlugin', yUndoPlugin());
} }
} }
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment