Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions backend/hyperglosae/src/updates/editFlag.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
function(doc, req) {
if (!doc) return [null, { json: { error: 'not_found' } }];
const body = JSON.parse(req.body);
if (body.beingEditedBy) {
doc.beingEditedBy = body.beingEditedBy;
} else {
delete doc.beingEditedBy;
}
return [doc, { json: { status: 'ok' } }];
}
6 changes: 3 additions & 3 deletions backend/hyperglosae/src/views/content/map.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
function (doc) {
const { getRelatedDocuments, emitPassages, emitIncludedDocuments } = require('views/lib/links');

let { _id, text = '', isPartOf = _id, links = [] } = doc;
let { _id, text = '', isPartOf = _id, links = [], beingEditedBy } = doc;
let related = getRelatedDocuments({isPartOf, links});

emitPassages({text, isPartOf, related});
emitPassages({text, isPartOf, related, beingEditedBy});
emitIncludedDocuments({isPartOf, links});
}
}
12 changes: 9 additions & 3 deletions backend/hyperglosae/src/views/lib/links.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,17 @@ const parseText = (text) => {
}));
}

exports.emitPassages = ({text, isPartOf, related}) => {
exports.emitPassages = ({text, isPartOf, related, beingEditedBy}) => {
parseText(text).forEach(({rubric, passage, parsed_rubric}) =>
related.forEach((x) => {
emit([x, ...parsed_rubric], { text: passage, isPartOf, rubric, _id: null });
})
emit([x, ...parsed_rubric], {
text: passage,
isPartOf,
rubric,
_id: null,
...(beingEditedBy && {beingEditedBy})
});
})
);
}

Expand Down
10 changes: 10 additions & 0 deletions frontend/scenarios/co-edit.feature
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,13 @@ Scénario: qui modifie les métadonnées
"""
Alors les métadonnées de la glose en mode édition contiennent "dc_creator: Bill"

Scénario: qui est en train de modifier le contenu
Quand "Bill" est en train d’éditer le passage “1”
Alors la glose en mode édition indique que "Bill" modifie le passage “1”

Scénario: termine de modifier le contenu
Soit "Bill" est en train d’éditer le passage “1”
Et la glose en mode édition indique que "Bill" modifie le passage “1”
Quand "Bill" quitte le mode édition
Alors la glose n'indique pas de modification en cours sur le passage "1"

44 changes: 38 additions & 6 deletions frontend/src/components/EditableText.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import DiscreeteDropdown from './DiscreeteDropdown';
import PictureUploadAction from '../menu-items/PictureUploadAction';
import {v4 as uuid} from 'uuid';
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
import { PencilSquare } from 'react-bootstrap-icons';

function EditableText({id, text, rubric, isPartOf, links, fragment, setFragment, setHighlightedText, setSelectedText, rawEditMode, setRawEditMode, backend, setLastUpdate}) {
function EditableText({id, text, rubric, isPartOf, links, beingEditedBy, fragment, setFragment, setHighlightedText, setSelectedText, rawEditMode, setRawEditMode, backend, setLastUpdate, user}) {
const [beingEdited, setBeingEdited] = useState(false);
const [editedDocument, setEditedDocument] = useState();
const [editedText, setEditedText] = useState();
const [hasBeenChanged, setHasBeenChanged] = useState(false);
const PASSAGE = new RegExp(`\\{${rubric}} ?([^{]*)`);
const isEditedByOther = beingEditedBy && beingEditedBy !== user;

let parsePassage = (rawText) => (rubric)
? rawText.match(PASSAGE)[1]
Expand All @@ -33,6 +35,19 @@ function EditableText({id, text, rubric, isPartOf, links, fragment, setFragment,
return x;
}), [backend, id, isPartOf, links, rubric]);

// Marque "en édition par <user>" quand on entre, démarque quand on sort
useEffect(() => {
if (!user || !id) return;
if (beingEdited) {
backend.markEditing(id, user).catch(console.error);
}
return () => {
if (beingEdited) {
backend.markEditing(id, null).catch(console.error);
}
};
}, [beingEdited, id, user, backend]);

useEffect(() => {
if (fragment) {
updateEditedDocument()
Expand All @@ -57,6 +72,7 @@ function EditableText({id, text, rubric, isPartOf, links, fragment, setFragment,
}, [rawEditMode, updateEditedDocument]);

let handleClick = () => {
if (isEditedByOther) return;
setBeingEdited(true);
updateEditedDocument()
.then((x) => {
Expand Down Expand Up @@ -102,25 +118,41 @@ function EditableText({id, text, rubric, isPartOf, links, fragment, setFragment,
.catch(console.error);
};

// Vue lecture
if (!beingEdited) return (
<div className="editable content position-relative">
<div className="editable content position-relative" id={id} >
{isEditedByOther && (
<OverlayTrigger
placement="top"
overlay={<Tooltip id={`being-edited-${id}`}>{beingEditedBy} is currently editing passage {rubric || '0'}</Tooltip>}
>
<PencilSquare className="being-edited-icon" data-testid="being-edited-icon" />
</OverlayTrigger>
)}
<OverlayTrigger
placement="top"
overlay={<Tooltip id={`tooltip-${id}`}>Edit content...</Tooltip>}
overlay={
<Tooltip id={`tooltip-${id}`}>
{isEditedByOther ? `Locked by ${beingEditedBy}` : 'Edit content...'}
</Tooltip>
}
>
<div className="formatted-text" onClick={handleClick}>
<FormattedText {...{setHighlightedText, setSelectedText}}>
{text || '&nbsp;'}
{text || '\u00A0'}
</FormattedText>
</div>
</OverlayTrigger>
<DiscreeteDropdown>
<PictureUploadAction {... {id, backend, handleImageUrl}}/>
<PictureUploadAction {...{id, backend, handleImageUrl}}/>
</DiscreeteDropdown>
</div>
);

// Vue édition
return (
<form>
<form className="position-relative">
<PencilSquare className="being-edited-icon self" data-testid="being-edited-self" />
<textarea className="form-control" type="text" rows="5" autoFocus
value={editedText} onChange={handleChange} onBlur={handleBlur}
/>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/OpenedDocuments.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ function OpenedDocuments({id, margin, metadata, parallelDocuments, rawEditMode,
</Row>
{parallelDocuments.passages.map(({rubric, source, scholia}, i) =>
<Passage key={rubric || i}
{...{source, rubric, scholia, margin, sourceId: id, isComposite, rawEditMode, setRawEditMode, backend, setLastUpdate}}
{...{source, rubric, scholia, margin, sourceId: id, isComposite, rawEditMode, setRawEditMode, backend, setLastUpdate, user}}
/>)
}
<Row>
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/components/Passage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import EditableText from '../components/EditableText';
import DiscreeteDropdown from './DiscreeteDropdown';
import CommentFragmentAction from '../menu-items/CommentFragmentAction';

function Passage({source, rubric, scholia, margin, sourceId, isComposite, rawEditMode, setRawEditMode, backend, setLastUpdate}) {
function Passage({source, rubric, scholia, margin, sourceId, isComposite, rawEditMode, setRawEditMode, backend, setLastUpdate, user}) {
const [selectedText, setSelectedText] = useState();
const [highlightedText, setHighlightedText] = useState('');
const [fragment, setFragment] = useState();
Expand Down Expand Up @@ -39,7 +39,7 @@ function Passage({source, rubric, scholia, margin, sourceId, isComposite, rawEdi
</Container>
}
</Col>
<PassageMargin active={!!margin} {...{scholia, rubric, setHighlightedText, fragment, setFragment, setSelectedText, rawEditMode, setRawEditMode, backend, setLastUpdate}} />
<PassageMargin active={!!margin} {...{scholia, rubric, setHighlightedText, fragment, setFragment, setSelectedText, rawEditMode, setRawEditMode, backend, setLastUpdate, user}} />
</Row>
);
}
Expand Down Expand Up @@ -87,12 +87,12 @@ function Rubric({id}) {
);
}

function PassageMargin({active, scholia, setHighlightedText, fragment, setFragment, setSelectedText, rawEditMode, setRawEditMode, backend, setLastUpdate}) {
function PassageMargin({active, scholia, setHighlightedText, fragment, setFragment, setSelectedText, rawEditMode, setRawEditMode, backend, setLastUpdate, user}) {
if (!active) return;
return (
<Col xs={5} className="scholium">
{scholia.map((scholium, i) =>
<EditableText key={i} {...scholium} {...{setHighlightedText, fragment, setFragment, setSelectedText, rawEditMode, setRawEditMode, backend, setLastUpdate}} />
<EditableText key={i} {...scholium} {...{setHighlightedText, fragment, setFragment, setSelectedText, rawEditMode, setRawEditMode, backend, setLastUpdate, user}} />
)}
</Col>
);
Expand Down
13 changes: 11 additions & 2 deletions frontend/src/hyperglosae.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ function Hyperglosae(logger) {
fetch(`${service}/${id}/${attachment.name}`, {
method: 'PUT',
headers: {
// ETag is the header that carries the current rev.
'If-Match': x.headers.get('ETag'),
'Content-Type': attachment.type
},
Expand Down Expand Up @@ -119,7 +118,17 @@ function Hyperglosae(logger) {
this.getView({view: 'all_documents', id: user || 'PUBLIC', options: ['include_docs']})
.then((rows) => rows.map(x => x.doc));

this.markEditing = (id, beingEditedBy) =>
fetch(`${service}/_design/app/_update/editFlag/${id}`, {
method: 'PUT',
body: JSON.stringify({ beingEditedBy })
}).then(x => x.json());

this.subscribeToChanges = (since = 'now') =>
fetch(`${service}/_changes?feed=longpoll&since=${since}&heartbeat=25000`)
.then(x => x.json());

return this;
}

export default Hyperglosae;
export default Hyperglosae;
7 changes: 6 additions & 1 deletion frontend/src/parallelDocuments.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,12 @@ function ParallelDocuments(id, rawContent = [], margin, raw = false) {
if (xor(!this.isFromScratch, isPartOf === id)) {
if (!raw || !part.scholia.length || part.scholia[part.scholia.length - 1].id !== x.id) {
let rubric = x.value.rubric;
part.scholia.push({id: x.id, text, isPartOf, ...(rubric !== '0' && {rubric})});

part.scholia.push({
id: x.id, text, isPartOf,
...(rubric !== '0' && {rubric}),
...(x.value.beingEditedBy && {beingEditedBy: x.value.beingEditedBy})
});
}
}
}
Expand Down
26 changes: 25 additions & 1 deletion frontend/src/routes/Lectern.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,30 @@ function Lectern({backend, user}) {
setParallelDocuments(new ParallelDocuments(id, content, margin, rawEditMode));
}, [id, content, margin, rawEditMode, lastUpdate]);

// Long polling sur les changements CouchDB
useEffect(() => {
let cancelled = false;
let since = 'now';
function poll() {
backend.subscribeToChanges(since)
.then(({ last_seq, results }) => {
if (cancelled) return;
since = last_seq;
if (results && results.length > 0) {
setLastUpdate(last_seq);
}
poll();
})
.catch(() => {
if (!cancelled) setTimeout(poll, 5000);
});
}
poll();
return () => {
cancelled = true;
};
}, [backend]);

if (!metadata?.focusedDocument?._id && !loading) {
return <DocumentNotFound />;
}
Expand Down Expand Up @@ -85,4 +109,4 @@ function References({metadata, active, createOn, setLastUpdate, backend, user})
);
}

export default Lectern;
export default Lectern;
11 changes: 11 additions & 0 deletions frontend/src/styles/EditableText.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,15 @@
border-color: black;
}

.being-edited-icon {
position: absolute;
top: 4px;
right: 4px;
color: #d2691e;
z-index: 5;
pointer-events: auto;
}

.being-edited-icon.self {
color: #2e7d32;
}
Loading