From 56addac0d6c858dfaaa13d357a6687e682bb037b Mon Sep 17 00:00:00 2001 From: Dinalrn Date: Tue, 21 Apr 2026 15:33:16 +0200 Subject: [PATCH 1/4] SCENARIO: Concurrent editors of the same document should be aware of which blocs are being edited (see #251). Co-authored-by: Dina LOUARN Nathan NICART Magdalena KHIAT --- frontend/scenarios/co-edit.feature | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frontend/scenarios/co-edit.feature b/frontend/scenarios/co-edit.feature index bcdcb8a9..9dc97179 100644 --- a/frontend/scenarios/co-edit.feature +++ b/frontend/scenarios/co-edit.feature @@ -22,3 +22,9 @@ 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 contient une icône + Et cette icône indique qu’il modifie le passage “1” From 1a86cc9da67fedf5640766c6318bedcc9eeff138 Mon Sep 17 00:00:00 2001 From: nathannct Date: Tue, 28 Apr 2026 16:39:58 +0200 Subject: [PATCH 2/4] SCENARIO: Concurrent editors of the same document should be aware of which blocs are being edited (see #251). Co-authored-by: Bleumms --- frontend/scenarios/co-edit.feature | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/frontend/scenarios/co-edit.feature b/frontend/scenarios/co-edit.feature index 9dc97179..7509d928 100644 --- a/frontend/scenarios/co-edit.feature +++ b/frontend/scenarios/co-edit.feature @@ -22,9 +22,14 @@ 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 contient une icône + Et cette icône indique qu’il modifie le passage “1” -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 contient une icône - Et cette icône indique qu’il 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 contient une icône + Et cette icône indique qu’il modifie le passage “1” + Quand "Bill" quitte le mode édition + Alors la glose qui était en mode édition ne contient plus d’icône \ No newline at end of file From 4a6ff1c465c6bfd76a151a8a61b5389572ca07ff Mon Sep 17 00:00:00 2001 From: Bleumms Date: Tue, 12 May 2026 15:31:50 +0200 Subject: [PATCH 3/4] SCENARIO: Concurrent editors of the same document should be aware of which blocs are being edited (see #251). Co-authored-by: nathanncrt --- frontend/scenarios/co-edit.feature | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/frontend/scenarios/co-edit.feature b/frontend/scenarios/co-edit.feature index 7509d928..72c40da3 100644 --- a/frontend/scenarios/co-edit.feature +++ b/frontend/scenarios/co-edit.feature @@ -24,12 +24,11 @@ Scénario: qui modifie les métadonnées 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 contient une icône - Et cette icône indique qu’il modifie 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 contient une icône - Et cette icône indique qu’il modifie 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 qui était en mode édition ne contient plus d’icône \ No newline at end of file + Alors la glose n'indique pas de modification en cours sur le passage "1" + From e225493ad6aaa6851305602bafad4e1fdd6ae5a9 Mon Sep 17 00:00:00 2001 From: Magdalena Date: Tue, 12 May 2026 17:39:52 +0200 Subject: [PATCH 4/4] IMPROVEMENT: Concurrent editors of the same document should be aware of which blocs are being edited (fixes #251) --- backend/hyperglosae/src/updates/editFlag.js | 10 +++++ backend/hyperglosae/src/views/content/map.js | 6 +-- backend/hyperglosae/src/views/lib/links.js | 12 ++++-- frontend/src/components/EditableText.jsx | 44 +++++++++++++++++--- frontend/src/components/OpenedDocuments.jsx | 2 +- frontend/src/components/Passage.jsx | 8 ++-- frontend/src/hyperglosae.js | 13 +++++- frontend/src/parallelDocuments.js | 7 +++- frontend/src/routes/Lectern.jsx | 26 +++++++++++- frontend/src/styles/EditableText.css | 11 +++++ 10 files changed, 118 insertions(+), 21 deletions(-) create mode 100644 backend/hyperglosae/src/updates/editFlag.js diff --git a/backend/hyperglosae/src/updates/editFlag.js b/backend/hyperglosae/src/updates/editFlag.js new file mode 100644 index 00000000..3ae8e855 --- /dev/null +++ b/backend/hyperglosae/src/updates/editFlag.js @@ -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' } }]; +} \ No newline at end of file diff --git a/backend/hyperglosae/src/views/content/map.js b/backend/hyperglosae/src/views/content/map.js index fcec13f7..1fdb95a6 100644 --- a/backend/hyperglosae/src/views/content/map.js +++ b/backend/hyperglosae/src/views/content/map.js @@ -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}); -} +} \ No newline at end of file diff --git a/backend/hyperglosae/src/views/lib/links.js b/backend/hyperglosae/src/views/lib/links.js index 3e01b765..dd38560c 100644 --- a/backend/hyperglosae/src/views/lib/links.js +++ b/backend/hyperglosae/src/views/lib/links.js @@ -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}) + }); + }) ); } diff --git a/frontend/src/components/EditableText.jsx b/frontend/src/components/EditableText.jsx index 19b3452a..6d3e4fac 100644 --- a/frontend/src/components/EditableText.jsx +++ b/frontend/src/components/EditableText.jsx @@ -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] @@ -33,6 +35,19 @@ function EditableText({id, text, rubric, isPartOf, links, fragment, setFragment, return x; }), [backend, id, isPartOf, links, rubric]); + // Marque "en édition par " 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() @@ -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) => { @@ -102,25 +118,41 @@ function EditableText({id, text, rubric, isPartOf, links, fragment, setFragment, .catch(console.error); }; + // Vue lecture if (!beingEdited) return ( -
+
+ {isEditedByOther && ( + {beingEditedBy} is currently editing passage {rubric || '0'}} + > + + + )} Edit content...} + overlay={ + + {isEditedByOther ? `Locked by ${beingEditedBy}` : 'Edit content...'} + + } >
- {text || ' '} + {text || '\u00A0'}
- +
); + + // Vue édition return ( -
+ +