From 92e4100774bd635daed9939e6af002438cbdab7c Mon Sep 17 00:00:00 2001 From: Dinalrn Date: Tue, 26 May 2026 15:00:15 +0200 Subject: [PATCH 1/4] SCENARIO: Metadata should be editable in a form rather than in a free text field (see #375). Co-authored-by: Dina LOUARN Co-authored-by: Magdalena KHIAT --- frontend/scenarios/edit_metadata.feature | 38 ++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/frontend/scenarios/edit_metadata.feature b/frontend/scenarios/edit_metadata.feature index 63bbfc3b..b2f0bff9 100644 --- a/frontend/scenarios/edit_metadata.feature +++ b/frontend/scenarios/edit_metadata.feature @@ -43,3 +43,41 @@ Scénario: sans être connecté dc_issued: 1932 """ Alors je peux lire "Before editing this document, please log in first" + + + + Scénario: avec un formulaire dont on est l'auteur + + Soit un document dont je suis l'auteur affiché comme glose + Et une session active avec mon compte + Quand j'ouvre le formulaire de modification des métadonnées + Et je remplis "title" avec "Chapitre 1: Contexte historique" + Et je remplis "creator" avec "Alice Liddell" + Et je remplis "issued" avec "1932" + Et je remplis "language" avec "french" + Et je remplis "translator" avec "Charles Beaudelaire" + Et je remplis "isPartOf" avec "Philosophie Moderne : Une première approche" + Et je valide le formulaire + Alors "Chapitre 1: Contexte historique" est la glose ouverte + Et le créateur est "Alice Liddell" + Et l'année de publication est "1932" + Et la langue est "French" + Et le titre de l'ouvrage est "Philosophie Moderne : Une première approche" + + +Scénario: avec un formulaire dont on n'est pas l'auteur + + Soit un document dont je ne suis pas l'auteur affiché comme glose + Et une session active avec mon compte + Quand j'ouvre le formulaire de modification des métadonnées + Alors je peux lire "Before editing this document, please request authorization to its editors first" + + + +Scénario: avec un formulaire sans être connecté + + Soit un document dont je suis l'auteur affiché comme glose + Quand j'ouvre le formulaire de modification des métadonnées + Alors je peux lire "Before editing this document, please log in first" + + From 52a8bca2665b3decc96abe4a257ae4bfdeb6c96c Mon Sep 17 00:00:00 2001 From: PA0Lbst Date: Tue, 9 Jun 2026 16:06:48 +0200 Subject: [PATCH 2/4] =?UTF-8?q?IMPLEMENTATION:=20Metadata=20should=20be=20?= =?UTF-8?q?editable=20in=20a=20form=20rather=20than=20in=20a=20free?= =?UTF-8?q?=E2=80=A6=20(see=20#375).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Nyrco --- frontend/src/components/Metadata.jsx | 137 ++++++++++++++++++++++++--- frontend/src/styles/Metadata.css | 28 ++++++ 2 files changed, 152 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/Metadata.jsx b/frontend/src/components/Metadata.jsx index 7cf39430..97f4a4ef 100644 --- a/frontend/src/components/Metadata.jsx +++ b/frontend/src/components/Metadata.jsx @@ -1,13 +1,28 @@ import '../styles/Metadata.css'; import { useEffect, useState } from 'react'; -import { parse, stringify } from 'yaml'; import { OverlayTrigger, Tooltip } from 'react-bootstrap'; -function Metadata({metadata = {}, editable, backend, setLastUpdate}) { +const CORE_FIELDS = [ + {key: 'dc_creator', label: 'Creator', type: 'text'}, + {key: 'dc_title', label: 'Title', type: 'text'}, + {key: 'dc_issued', label: 'Date', type: 'date'}, +]; + +const OPTIONAL_FIELDS = [ + {key: 'dc_translator', label: 'Translator', type: 'text'}, + {key: 'dc_language', label: 'Language', type: 'text'}, + {key: 'dc_isPartOf', label: 'Part of', type: 'text'}, + {key: 'dc_publisher', label: 'Publisher', type: 'text'}, + {key: 'dc_spatial', label: 'Spatial', type: 'text'}, +]; + +function Metadata({metadata = {}, editable, user, backend, setLastUpdate}) { const [beingEdited, setBeingEdited] = useState(false); const [editedDocument, setEditedDocument] = useState(metadata); - const [editedText, setEditedText] = useState(); + const [editedMetadata, setEditedMetadata] = useState({}); + const [shownOptional, setShownOptional] = useState([]); + const [showAddMenu, setShowAddMenu] = useState(false); useEffect(() => { setEditedDocument(metadata); @@ -18,33 +33,78 @@ function Metadata({metadata = {}, editable, backend, setLastUpdate}) { Object.entries(doc).filter(([key, _]) => key.startsWith('dc_')) ); + let toFieldValue = (value) => + Array.isArray(value) ? value.join(' & ') : (value ?? ''); + + let toDateInputValue = (value) => { + if (!value) return ''; + let date = new Date(value.toString()); + return isNaN(date) ? '' : date.toISOString().slice(0, 10); + }; + + let fieldValue = (key, type) => + type === 'date' + ? toDateInputValue(editedMetadata[key]) + : toFieldValue(editedMetadata[key]); + + let isAuthorized = (doc) => + user && (!doc.editors || doc.editors.includes(user)); + let handleClick = () => { backend.getDocument(metadata._id) .then((x) => { + let md = getMetadata(x); setEditedDocument(x); - setEditedText(stringify(getMetadata(x))); + setEditedMetadata(md); + // Reveal optional fields that already hold a value. + setShownOptional( + OPTIONAL_FIELDS.filter(({key}) => md[key]).map(({key}) => key) + ); + setShowAddMenu(false); setBeingEdited(true); + // Surface the canonical authorization warning right away; the backend + // rejects this no-op write so no revision is created. + if (!isAuthorized(x)) backend.putDocument(x).catch(() => {}); }); }; - let handleChange = (event) => { - setEditedText(event.target.value); + let handleFieldChange = (key) => (event) => { + setEditedMetadata((current) => ({...current, [key]: event.target.value})); }; - let handleBlur = () => { - setBeingEdited(false); + let handleAddField = (key) => { + setShownOptional((current) => [...current, key]); + setShowAddMenu(false); + }; + + let save = () => { let updatedDocument = { ...Object.fromEntries( Object.entries(editedDocument).filter(([key, _]) => !key.startsWith('dc_')) ), - ...parse(editedText.replaceAll(/(dc_isPartOf|dc_title):\s*"?([^"\n]+)"?$/gm, '$1: "$2"')) + ...Object.fromEntries( + Object.entries(editedMetadata).filter(([_, value]) => value !== '') + ), }; setEditedDocument(updatedDocument); + setBeingEdited(false); + setShowAddMenu(false); backend.putDocument(updatedDocument) .then(x => setLastUpdate(x.rev)) .catch(console.error); }; + let handleSubmit = (event) => { + event.preventDefault(); + save(); + }; + + let handleBlur = (event) => { + // Only save once focus leaves the whole form, not when moving between fields. + if (event.currentTarget.contains(event.relatedTarget)) return; + save(); + }; + let format = (actors, prefix = '', suffix = '') => actors && (prefix + [actors].flat().join(' & ') + suffix); @@ -86,11 +146,62 @@ function Metadata({metadata = {}, editable, backend, setLastUpdate}) { ); } + let visibleFields = [ + ...CORE_FIELDS, + ...OPTIONAL_FIELDS.filter(({key}) => shownOptional.includes(key)), + ]; + let addableFields = OPTIONAL_FIELDS.filter(({key}) => !shownOptional.includes(key)); + return ( -
-