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
40 changes: 40 additions & 0 deletions frontend/scenarios/edit_metadata.feature
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,43 @@ 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 "creator" avec "Alice Liddell"
Et je remplis "title" avec "Chapitre 1: Contexte historique"
Et je remplis "issued" avec "1932"
Et j'ajoute le champ "Language" au formulaire
Et je remplis "language" avec "french"
Et j'ajoute le champ "Translator" au formulaire
Et je remplis "translator" avec "Charles Beaudelaire"
Et j'ajoute le champ "Part of" au formulaire
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"


139 changes: 125 additions & 14 deletions frontend/src/components/Metadata.jsx
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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);

Expand All @@ -69,7 +129,7 @@ function Metadata({metadata = {}, editable, backend, setLastUpdate}) {
<span className="edition">
{formatTranslation(dc_translator, dc_language)}
{dc_isPartOf ? <i>{dc_isPartOf}, </i> : ''}
{dc_issued ? `${new Date(dc_issued.toString()).getFullYear()}` : ''}
{dc_issued ? `${dc_issued.toString().slice(0, 4)}` : ''}
</span>
</>
);
Expand All @@ -86,11 +146,62 @@ function Metadata({metadata = {}, editable, backend, setLastUpdate}) {
</span>
);
}
let visibleFields = [
...CORE_FIELDS,
...OPTIONAL_FIELDS.filter(({key}) => shownOptional.includes(key)),
];
let addableFields = OPTIONAL_FIELDS.filter(({key}) => !shownOptional.includes(key));

return (
<form>
<textarea className="form-control" type="text" rows="5" autoFocus
value={editedText} onChange={handleChange} onBlur={handleBlur}
/>
<form className="metadata-form" onSubmit={handleSubmit} onBlur={handleBlur}>
{visibleFields.map(({key, label, type}, index) => (
<div className="metadata-field mb-2" key={key}>
<label className="form-label" htmlFor={key}>{label}</label>
<input
id={key}
className="form-control"
type={type}
autoFocus={index === 0}
value={fieldValue(key, type)}
onChange={handleFieldChange(key)}
/>
</div>
))}
{addableFields.length > 0 && (
<div className="add-section position-relative">
{/* preventDefault keeps focus on the inputs so the onBlur save does
not fire (and close the form) while interacting with this menu. */}
<button
type="button"
className="btn btn-outline-secondary btn-sm add-section-toggle"
aria-label="Add a section"
onMouseDown={(event) => {
event.preventDefault();
setShowAddMenu((v) => !v);
}}
>
+
</button>
{showAddMenu && (
<ul className="dropdown-menu show">
{addableFields.map(({key, label}) => (
<li key={key}>
<button
type="button"
className="dropdown-item"
onMouseDown={(event) => {
event.preventDefault();
handleAddField(key);
}}
>
{label}
</button>
</li>
))}
</ul>
)}
</div>
)}
</form>
);
}
Expand Down
28 changes: 28 additions & 0 deletions frontend/src/styles/Metadata.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,34 @@
color: inherit;
}

.metadata-form .metadata-field {
display: flex;
align-items: center;
gap: .5rem;
}

.metadata-form .metadata-field .form-label {
width: 7rem;
flex-shrink: 0;
margin-bottom: 0;
text-align: left;
}

.metadata-form .metadata-field .form-label::after {
content: " :";
}

.metadata-form .add-section {
text-align: left;
}

.metadata-form .add-section .dropdown-menu {
position: absolute;
top: 100%;
left: 0;
display: block;
}

.icon {
opacity: 1 !important;
cursor: pointer;
Expand Down
32 changes: 29 additions & 3 deletions frontend/tests/event.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@ Quand("j'ouvre {string} à côté", (title) => {
cy.contains('span', title).parent().prevAll('a.open').first().click();
});

Quand("j'essaie de remplacer les métadonnées de la glose par :", (metadata) => {
cy.edit_metadata(metadata);
});


Quand("j'essaie de remplacer l'annotation du passage {int} par :", (block_number, markdown) => {
let element = cy.get(`.lectern>.row:nth-child(${block_number + 1})>.scholium>.content>.formatted-text`);
Expand Down Expand Up @@ -156,6 +154,12 @@ Quand("{string} remplace les métadonnées de la glose par :", (username, metada
cy.request_by_user(username, parseStrToObject(metadata));
});


Quand("j'ouvre le formulaire de modification des métadonnées", () => { cy.click_on_text('metadata'); });


Quand("je valide le formulaire", () => { cy.get('.metadata-form').submit();
});
Quand("le titre de l'onglet est {string}", (title) => {
cy.title().should("eq", title);
});
Expand All @@ -173,3 +177,25 @@ Quand("j'essaie de créer une glose qui soit découpée en passages", () => {
cy.click_on_create();
cy.get('.scholium .focus').click();
});

//MOFIER PAR MOI
Quand("je remplis {string} avec {string}", (field, value) => {
const input = cy.get(`#dc_${field}`);
// dc_issued est de type "date" : on force la valeur via invoke('val') + trigger
if (field === 'issued') {
const dateValue = value.length === 4 ? `${value}-01-01` : value;
input.invoke('val', dateValue).trigger('input').trigger('change');
} else {
input.clear().type(value);
}
});



Quand("j'ajoute le champ {string} au formulaire", (label) => {
cy.get('.add-section-toggle').click();
cy.contains('.dropdown-item', label).click();
});

//

8 changes: 5 additions & 3 deletions frontend/tests/outcome.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,6 @@ Alors("l'année de publication est {string}", (year) => {
cy.get('.metadata > .edition').first().should('contain', year);
});

Alors("le titre de l'ouvrage est {string}", (title) => {
cy.get('.metadata > .edition').first().should('contain', title);
})

Alors("la langue est {string}", (language) => {
cy.get('.metadata > .edition').first().should('contain', language);
Expand Down Expand Up @@ -222,3 +219,8 @@ Alors("le document apparaît une seule fois dans la liste de ma bibliothèque",
cy.get('.bookshelf').contains(this.randomName);
cy.get('.bookshelf .work').filter(`:contains("${this.randomName}")`).should('have.length', 1);
});


Alors("le titre de l'ouvrage est {string}", (title) => {
cy.get('.metadata').should('contain', title);
});
Loading