This commit is contained in:
Rainer Simon 2020-12-21 13:35:28 +01:00
parent ae07174ea5
commit ac1e27678d
4 changed files with 50 additions and 60 deletions

View File

@ -41,8 +41,8 @@ const Editor = props => {
// on move. Therefore, don't update if a) props.annotation equals // on move. Therefore, don't update if a) props.annotation equals
// the currentAnnotation, or props.annotation and currentAnnotations are // the currentAnnotation, or props.annotation and currentAnnotations are
// a selection, just created by the user. // a selection, just created by the user.
const preventUpdate = setCurrentAnnotation.isSelection ? const preventUpdate = currentAnnotation?.isSelection ?
annotation.isSelection : currentAnnotation.annotationId === annotation.annotationId; annotation?.isSelection : currentAnnotation?.id === annotation.id;
if (!preventUpdate) if (!preventUpdate)
setCurrentAnnotation(annotation); setCurrentAnnotation(annotation);
@ -77,9 +77,8 @@ const Editor = props => {
const { user } = props.env; const { user } = props.env;
// Metadata is only added when a user is set, otherwise // Metadata is only added when a user is set, otherwise
// the Editor operates in 'anonymous mode'. Also, // the Editor operates in 'anonymous mode'.
// no point in adding meta while we're in draft state if (user) {
if (!body.draft && user) {
meta.creator = {}; meta.creator = {};
if (user.id) meta.creator.id = user.id; if (user.id) meta.creator.id = user.id;
if (user.displayName) meta.creator.name = user.displayName; if (user.displayName) meta.creator.name = user.displayName;
@ -116,11 +115,10 @@ const Editor = props => {
props.onCancel(currentAnnotation); props.onCancel(currentAnnotation);
const onOk = _ => { const onOk = _ => {
// Removes the 'draft' flag from all bodies // Removes the state payload from all bodies
const undraft = annotation => const undraft = annotation =>
annotation.clone({ annotation.clone({
body : annotation.bodies.map(({ draft, ...rest }) => body : annotation.bodies.map(({ draft, ...rest }) => rest)
draft ? { ...rest, ...creationMeta(rest) } : rest )
}); });
// Current annotation is either a selection (if it was created from // Current annotation is either a selection (if it was created from

View File

@ -3,11 +3,10 @@ import { useState } from 'preact/hooks';
import TimeAgo from 'timeago-react'; import TimeAgo from 'timeago-react';
import DropdownMenu from './DropdownMenu'; import DropdownMenu from './DropdownMenu';
import TextEntryField from './TextEntryField'; import TextEntryField from './TextEntryField';
import PurposeDropdown from './PurposeDropdown'; import PurposeSelect from './PurposeSelect';
import { ChevronDownIcon } from '../../../Icons'; import { ChevronDownIcon } from '../../../Icons';
import i18n from '../../../i18n'; import i18n from '../../../i18n';
/** A single comment inside the CommentWidget **/ /** A single comment inside the CommentWidget **/
const Comment = props => { const Comment = props => {
@ -15,7 +14,6 @@ const Comment = props => {
const [ isMenuVisible, setIsMenuVisible ] = useState(false); const [ isMenuVisible, setIsMenuVisible ] = useState(false);
const onMakeEditable = _ => { const onMakeEditable = _ => {
props.body.draft = true;
setIsEditable(true); setIsEditable(true);
setIsMenuVisible(false); setIsMenuVisible(false);
} }
@ -25,13 +23,13 @@ const Comment = props => {
setIsMenuVisible(false); setIsMenuVisible(false);
} }
const onUpdateComment = evt => { const onUpdateComment = evt =>
props.onUpdate(props.body, { ...props.body, value: evt.target.value }); props.onUpdate(props.body, { ...props.body, value: evt.target.value });
}
const onUpdateDropdown = evt => { const onChangePurpose = evt =>
props.onUpdate(props.body, { ...props.body, purpose: evt.value }); props.onUpdate(props.body, { ...props.body, purpose: evt.value });
}
const timestamp = props.body.modified || props.body.created;
const creatorInfo = props.body.creator && const creatorInfo = props.body.creator &&
<div className="r6o-lastmodified"> <div className="r6o-lastmodified">
@ -39,7 +37,7 @@ const Comment = props => {
{ props.body.created && { props.body.created &&
<span className="r6o-lastmodified-at"> <span className="r6o-lastmodified-at">
<TimeAgo <TimeAgo
datetime={props.env.toClientTime(props.body.created)} datetime={props.env.toClientTime(timestamp)}
locale={i18n.locale()} /> locale={i18n.locale()} />
</span> </span>
} }
@ -58,13 +56,13 @@ const Comment = props => {
onChange={onUpdateComment} onChange={onUpdateComment}
onSaveAndClose={props.onSaveAndClose} onSaveAndClose={props.onSaveAndClose}
/> />
{ creatorInfo } { !isEditable && creatorInfo }
{ props.purpose && { props.purposeSelector &&
<PurposeDropdown <PurposeSelect
editable={isEditable} editable={isEditable}
content={props.body.purpose} content={props.body.purpose}
onChange={onUpdateDropdown} onChange={onChangePurpose}
onSaveAndClose={props.onSaveAndClose} onSaveAndClose={props.onSaveAndClose}
/> } /> }

View File

@ -2,47 +2,47 @@ import React from 'preact/compat';
import Comment from './Comment'; import Comment from './Comment';
import TextEntryField from './TextEntryField'; import TextEntryField from './TextEntryField';
import i18n from '../../../i18n'; import i18n from '../../../i18n';
import PurposeDropdown, {purposes} from './PurposeDropdown'; import PurposeSelect, { PURPOSES } from './PurposeSelect';
const validPurposes = PURPOSES.map(p => p.value);
const purposeValues = purposes.map(p => p.value);
/** /**
* Comments are TextualBodies where the purpose field is either * Comments are TextualBodies where the purpose field is either
* blank or 'commenting' or 'replying' * blank or 'commenting' or 'replying'
*/ */
const isComment = (body, purposenabled) => const isComment = (body, matchAllPurposes) => {
body.type === 'TextualBody' && ( const hasMatchingPurpose = matchAllPurposes ?
!body.hasOwnProperty('purpose') || purposeCheck(body.purpose, purposenabled) validPurposes.indexOf(body.purpose) > -1 : body.purpose == 'commenting' || body.purpose == 'replying';
return body.type === 'TextualBody' && (
!body.hasOwnProperty('purpose') || hasMatchingPurpose
); );
}
/** /**
* The draft reply is a comment body with a 'draft' flag * The draft reply is a comment body with a 'draft' flag
*/ */
const purposeCheck = (purpose, purposenabled) => {
if (purposenabled){
return purposeValues.indexOf(purpose) > -1
} else {
return purpose == 'commenting' || purpose == 'replying'
}
}
const getDraftReply = (existingDraft, isReply) => { const getDraftReply = (existingDraft, isReply) => {
let draft = existingDraft ? existingDraft : { const purpose = isReply ? 'replying' : 'commenting';
type: 'TextualBody', value: '', draft: true return existingDraft ? existingDraft : {
type: 'TextualBody', value: '', purpose, draft: true
}; };
draft.purpose = draft.purpose ? draft.purpose : isReply ? 'replying' : 'commenting';
return draft;
}; };
/** /**
* Renders a list of comment bodies, followed by a 'reply' field. * Renders a list of comment bodies, followed by a 'reply' field.
*/ */
const CommentWidget = props => { const CommentWidget = props => {
// All comments (draft + non-draft) // All comments
const all = props.annotation ? const all = props.annotation ?
props.annotation.bodies.filter(body => isComment(body, props.purpose)) : []; props.annotation.bodies.filter(body => isComment(body, props.purposeSelector)) : [];
// Last draft comment without a creator field goes into the reply field
const draftReply = getDraftReply(all.slice().reverse().find(b => b.draft && !b.creator), all.length > 1); // Add a draft reply if there isn't one already
const draftReply = getDraftReply(all.find(b => b.draft == true), all.length > 1);
// All except draft reply // All except draft reply
const comments = all.filter(b => b != draftReply); const comments = all.filter(b => b != draftReply);
const onEditReply = evt => { const onEditReply = evt => {
const prev = draftReply.value; const prev = draftReply.value;
const updated = evt.target.value; const updated = evt.target.value;
@ -56,14 +56,8 @@ const CommentWidget = props => {
} }
} }
const onUpdatePurpose = evt => { const onChangeReplyPurpose = purpose =>
const updated = evt.value.trim(); props.onUpdateBody(draftReply, { ...draftReply, purpose: purpose.value });
if (draftReply.value == '' && updated.length > 0) {
draftReply.purpose = updated;
} else {
props.onUpdateBody(draftReply, { ...draftReply, purpose: updated });
}
}
// A comment should be read-only if: // A comment should be read-only if:
// - the global read-only flag is set // - the global read-only flag is set
@ -96,7 +90,7 @@ const CommentWidget = props => {
<Comment <Comment
key={idx} key={idx}
env={props.env} env={props.env}
purpose={props.purpose} purposeSelector={props.purposeSelector}
readOnly={isReadOnly(body)} readOnly={isReadOnly(body)}
body={body} body={body}
onUpdate={props.onUpdateBody} onUpdate={props.onUpdateBody}
@ -113,11 +107,11 @@ const CommentWidget = props => {
onChange={onEditReply} onChange={onEditReply}
onSaveAndClose={() => props.onSaveAndClose()} onSaveAndClose={() => props.onSaveAndClose()}
/> />
{ props.purpose == true && draftReply.value.length > 0 && { props.purposeSelector && draftReply.value.length > 0 &&
<PurposeDropdown <PurposeSelect
editable={true} editable={true}
content={draftReply.purpose} content={draftReply.purpose}
onChange={onUpdatePurpose} onChange={onChangeReplyPurpose}
onSaveAndClose={() => props.onSaveAndClose()} onSaveAndClose={() => props.onSaveAndClose()}
/> />
} }

View File

@ -1,7 +1,7 @@
import React from 'preact/compat'; import React from 'preact/compat';
import Select from 'react-select'; import Select from 'react-select';
export const purposes = [ export const PURPOSES = [
{'value': 'assessing', 'label': 'Assessing'}, {'value': 'assessing', 'label': 'Assessing'},
{'value': 'bookmarking', 'label': 'Bookmarking'}, {'value': 'bookmarking', 'label': 'Bookmarking'},
{'value': 'classifying', 'label': 'Classifying'}, {'value': 'classifying', 'label': 'Classifying'},
@ -16,17 +16,17 @@ export const purposes = [
{'value': 'replying', 'label': 'Replying'} {'value': 'replying', 'label': 'Replying'}
] ]
const PurposeDropdown = props => { const PurposeSelect = props => {
const selectedOption = props.content ? const selectedOption = props.content ?
purposes.find(p => p.value === props.content) : null; PURPOSES.find(p => p.value === props.content) : null;
return ( return (
<div class="r6o-purposedropdown"> <div class="r6o-purposedropdown">
<Select <Select
value={selectedOption} value={selectedOption}
onChange={props.onChange} onChange={props.onChange}
options={purposes} options={PURPOSES}
isDisabled={!props.editable} isDisabled={!props.editable}
/> />
</div> </div>
@ -34,4 +34,4 @@ const PurposeDropdown = props => {
} }
export default PurposeDropdown; export default PurposeSelect;