diff --git a/dataedit/static/peer_review/navigation.js b/dataedit/static/peer_review/navigation.js
index c813d8fa3..bd90000f2 100644
--- a/dataedit/static/peer_review/navigation.js
+++ b/dataedit/static/peer_review/navigation.js
@@ -7,29 +7,34 @@
import { getCategoryToTabIdMapping, makeFieldList, selectField } from "./peer_review.js";
import { isEmptyValue, isEffectivelyEmpty, sendJson } from "./utilities.js";
+
export function updateTabProgress() {
const allFields = document.querySelectorAll('.review__item');
- const total = allFields.length;
- let completed = 0;
+ let total = 0;
+ let accepted = 0;
allFields.forEach(field => {
- // Check for any of the completed states (ok, rejected, suggestion)
- if (field.classList.contains('field-ok') ||
- field.classList.contains('field-rejected') ||
- field.classList.contains('field-suggestion')) {
- completed++;
+ const fieldKey = field.dataset.fieldkey;
+ const fieldValue = field.dataset.fieldvalue;
+
+ // Only count fields that actually need review (not effectively empty)
+ if (!isEffectivelyEmpty(fieldKey, fieldValue)) {
+ total++;
+
+ // Only count accepted (ok) fields as progress
+ if (field.classList.contains('field-ok')) {
+ accepted++;
+ }
}
});
- const percentage = total === 0 ? 0 : Math.round((completed / total) * 100);
+ const percentage = total === 0 ? 0 : Math.round((accepted / total) * 100);
- // Update the circular progress bar
const circle = document.getElementById('okProgressCircle');
const text = document.getElementById('okPercentageText');
-
+
if (circle && text) {
- // 326.72 is 2*PI*r where r=52 (from your SVG)
- const circumference = 326.72;
+ const circumference = 326.72;
const offset = circumference - (percentage / 100) * circumference;
circle.style.strokeDashoffset = offset;
text.textContent = `${percentage}%`;
diff --git a/dataedit/static/peer_review/opr_reviewer.js b/dataedit/static/peer_review/opr_reviewer.js
index e0682cf19..bb96e21bf 100644
--- a/dataedit/static/peer_review/opr_reviewer.js
+++ b/dataedit/static/peer_review/opr_reviewer.js
@@ -139,6 +139,78 @@ function deletePeerReview() {
});
}
+/**
+ * Expands all ancestor accordion panels containing the field element,
+ * then scrolls the field into view once they are open.
+ */
+function expandAccordionsAndScrollToField(fieldKey) {
+ const fieldElement = document.querySelector(`.field[data-fieldkey="${fieldKey}"]`);
+ if (!fieldElement) return;
+
+ // Collect all collapsed accordion-collapse ancestors
+ const collapsedAncestors = [];
+ let parent = fieldElement.parentElement;
+
+ while (parent) {
+ if (
+ parent.classList.contains('accordion-collapse') &&
+ !parent.classList.contains('show')
+ ) {
+ collapsedAncestors.push(parent);
+ }
+ parent = parent.parentElement;
+ }
+
+ if (collapsedAncestors.length === 0) {
+ // No accordions to open, scroll immediately
+ scrollToField(fieldElement);
+ return;
+ }
+
+ // Reverse so we open outermost first, then inner
+ collapsedAncestors.reverse();
+
+ // Open each accordion in sequence, waiting for each transition to finish
+ function openNext(index) {
+ if (index >= collapsedAncestors.length) {
+ // All open, now scroll
+ scrollToField(fieldElement);
+ return;
+ }
+
+ const collapseEl = collapsedAncestors[index];
+
+ // Find the toggle button for this accordion panel
+ const toggleButton = document.querySelector(
+ `[data-bs-target="#${collapseEl.id}"]`
+ );
+
+ if (!toggleButton) {
+ // No button found, try next
+ openNext(index + 1);
+ return;
+ }
+
+ // Listen for when this panel finishes opening
+ collapseEl.addEventListener('shown.bs.collapse', function handler() {
+ collapseEl.removeEventListener('shown.bs.collapse', handler);
+ openNext(index + 1);
+ });
+
+ // Click the toggle to open it
+ toggleButton.click();
+ }
+
+ openNext(0);
+}
+
+/**
+ * Scrolls the field element into view smoothly, centered vertically.
+ */
+function scrollToField(fieldElement) {
+ fieldElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
+}
+
function click_field(fieldKey, fieldValue, category) {
const isEmpty = isEffectivelyEmpty(fieldKey, fieldValue);
@@ -151,24 +223,18 @@ function click_field(fieldKey, fieldValue, category) {
setselectedFieldValue(fieldValue);
setSelectedCategory(category);
- // Build the lookup key for field_descriptions_json.
- // The fieldKey from the HTML has resources.N. stripped already,
- // e.g. "spatial.extent.name". The schema stores it under
- // "resources.spatial.extent.name". So we try both, plus
- // a version with numeric indices removed.
const cleanedFieldKey = fieldKey.replace(/\.\d+/g, '');
- // Try to find description using multiple key variants
const candidateKeys = [
- `resources.${category}.${fieldKey}`, // resources.spatial.extent.name
- `resources.${category}.${cleanedFieldKey}`, // resources.spatial.extent.name
- `resources.${fieldKey}`, // fallback
- `resources.${cleanedFieldKey}`, // fallback
- fieldKey,
- cleanedFieldKey,
-];
-
- let resolvedKey = cleanedFieldKey; // default fallback
+ `resources.${category}.${fieldKey}`,
+ `resources.${category}.${cleanedFieldKey}`,
+ `resources.${fieldKey}`,
+ `resources.${cleanedFieldKey}`,
+ fieldKey,
+ cleanedFieldKey,
+ ];
+
+ let resolvedKey = cleanedFieldKey;
if (typeof fieldDescriptionsData !== 'undefined' && fieldDescriptionsData) {
for (const candidate of candidateKeys) {
if (fieldDescriptionsData[candidate]) {
@@ -199,11 +265,35 @@ function click_field(fieldKey, fieldValue, category) {
if (valueEl) valueEl.style.color = '';
}
+ // Always start fresh visually
clearInputFields();
hideReviewerOptions();
hideReviewerCommentsOptions();
-}
+ // --- NEW: Restore previous review entry if it exists ---
+ const existingReview = current_review.reviews.find(r => r.key === fieldKey);
+ if (existingReview && existingReview.fieldReview) {
+ const fr = existingReview.fieldReview;
+ const previousState = fr.state;
+
+ // Simulate clicking the correct state button to show the right UI
+ if (previousState === 'suggestion') {
+ showReviewerOptions();
+ hideReviewerCommentsOptions();
+ const valuearea = document.getElementById('valuearea');
+ const commentarea = document.getElementById('commentarea');
+ if (valuearea) valuearea.value = fr.newValue || '';
+ if (commentarea) commentarea.value = fr.comment || '';
+ } else if (previousState === 'rejected') {
+ hideReviewerOptions();
+ showReviewerCommentsOptions();
+ const comments = document.getElementById('comments');
+ if (comments) comments.value = fr.additionalComment || '';
+ }
+ // For 'ok', no extra UI needed, fields stay hidden
+ }
+ expandAccordionsAndScrollToField(fieldKey);
+}
function updateFieldColor(fieldKey, state) {
const safeId = '#field_' + fieldKey.replace(/\./g, "\\.");
$(safeId).removeClass('field-ok field-suggestion field-rejected');
@@ -267,16 +357,23 @@ function saveEntrancesForReviewer() {
updateClientStateDict(currentKey, selectedState);
// Update DOM suggestions immediately
- const fieldElement = document.getElementById("field_" + currentKey);
- if (fieldElement) {
- const suggEl = fieldElement.querySelector('.suggestion--highlight');
- if (suggEl) suggEl.innerText = reviewObj.reviewerSuggestion;
-
- const commEl = fieldElement.querySelector('.suggestion--additional-comment');
- if (commEl) commEl.innerText = reviewObj.additionalComment;
- }
+ const fieldElement = document.getElementById("field_" + currentKey);
+ if (fieldElement) {
+ const suggEl = fieldElement.querySelector('.suggestion--highlight');
+ if (suggEl) suggEl.innerText = reviewObj.reviewerSuggestion;
+
+ // ADD THIS: show the comment under the suggested value
+ const suggCommentEl = fieldElement.querySelector('.suggestion--comment');
+ if (suggCommentEl) {
+ suggCommentEl.innerText = (selectedState === 'suggestion') ? reviewObj.comment : '';
+ }
+
+ const commEl = fieldElement.querySelector('.suggestion--additional-comment');
+ if (commEl) commEl.innerText = reviewObj.additionalComment;
+ }
}
+
document.getElementById("comments").value = "";
checkReviewComplete();
renderSummaryPageFields();
diff --git a/dataedit/static/peer_review/summary.js b/dataedit/static/peer_review/summary.js
index 7640bcac7..111183db5 100644
--- a/dataedit/static/peer_review/summary.js
+++ b/dataedit/static/peer_review/summary.js
@@ -4,9 +4,9 @@
// SPDX-FileCopyrightText: 2026 Vismaya Jochem © Reiner Lemoine Institut
// SPDX-License-Identifier: AGPL-3.0-or-later
-import {current_review, selectedState} from "./peer_review.js";
-import {getFieldState} from "./state_current_review.js";
-import {isEmptyValue, isEffectivelyEmpty, sendJson} from "./utilities.js";
+import { current_review, selectedState } from "./peer_review.js";
+import { getFieldState } from "./state_current_review.js";
+import { isEmptyValue, isEffectivelyEmpty, sendJson } from "./utilities.js";
import { updatePercentageDisplay } from "./navigation.js";
export function renderSummaryPageFields() {
const acceptedFields = [];
@@ -14,69 +14,123 @@ export function renderSummaryPageFields() {
const rejectedFields = [];
const missingFields = [];
const emptyFields = [];
+
const processedFields = new Set();
- if (window.state_dict && Object.keys(window.state_dict).length > 0) {
- const fields = document.querySelectorAll('.field');
+ if (window.state_dict && Object.keys(window.state_dict).length > 0) {
+ const fields = document.querySelectorAll(".field");
for (let field of fields) {
- const field_id = field.id.slice(6);
- const fieldValue = $(field).find('.value').text().replace(/\s+/g, ' ').trim();
+ let field_id = field.id.slice(6);
+ const fieldValue = $(field)
+ .find(".value")
+ .text()
+ .replace(/\s+/g, " ")
+ .trim();
const fieldState = getFieldState(field_id);
- const fieldCategory = field.getAttribute('data-category');
- const fieldSuggestion = field.querySelector('.suggestion.suggestion--highlight')?.textContent.trim() || "";
-
- // remove the numbers and replace the dots with spaces
- let fieldName = field_id.replace(/\./g, ' ');
+ const fieldCategory = field.getAttribute("data-category");
+ const fieldSuggestion =
+ field
+ .querySelector(".suggestion.suggestion--highlight")
+ ?.textContent.trim() || "";
+
+ // ADD THIS: read comment from DOM just like fieldSuggestion
+ const fieldComment =
+ field
+ .querySelector(".suggestion--comment")
+ ?.textContent.trim() ||
+ field
+ .querySelector(".suggestion--additional-comment")
+ ?.textContent.trim() || "";
+
+ let fieldName = field_id.replace(/\./g, " ");
if (fieldCategory !== "general") {
- fieldName = fieldName.split(' ').slice(1).join(' '); // remove first word
+ fieldName = fieldName.split(" ").slice(1).join(" ");
}
const uniqueFieldIdentifier = `${fieldName}-${fieldCategory}`;
if (isEffectivelyEmpty(field_id, fieldValue)) {
- emptyFields.push({ fieldName, fieldValue, fieldCategory, fieldSuggestion });
- } else if (fieldState === 'ok') {
- acceptedFields.push({ fieldName, fieldValue, fieldCategory, fieldSuggestion });
+ emptyFields.push({
+ fieldName,
+ fieldValue,
+ fieldCategory,
+ fieldSuggestion,
+ fieldComment, // now defined
+ });
+ } else if (fieldState === "ok") {
+ acceptedFields.push({
+ fieldName,
+ fieldValue,
+ fieldCategory,
+ fieldSuggestion,
+ fieldComment, // now defined
+ });
processedFields.add(uniqueFieldIdentifier);
}
- }
- }
- for (const review of current_review.reviews) {
- const fieldDomId = `field_${review.key}`;
- const fieldEl = document.getElementById(fieldDomId);
- const fieldValue = fieldEl
- ? $(fieldEl).find('.value').text().replace(/\s+/g, ' ').trim()
- : "";
- const fieldState = review.fieldReview.state;
- const fieldCategory = review.category;
- const fieldSuggestion = review.fieldReview.reviewerSuggestion || "";
-
- let fieldName = review.key.replace(/\./g, ' ');
+ for (const review of current_review.reviews) {
+ const fieldDomId = `field_${review.key}`;
+ const fieldEl = document.getElementById(fieldDomId);
+ const fieldValue = fieldEl
+ ? $(fieldEl).find(".value").text().replace(/\s+/g, " ").trim()
+ : "";
+ const fieldState = review.fieldReview.state;
+ const fieldCategory = review.category;
+ const field_id = field.id.slice(6);
+ const fieldSuggestion = review.fieldReview.reviewerSuggestion || "";
+ const fieldComment = review.fieldReview.comment || review.fieldReview.additionalComment || "";
+
+ let fieldName = review.key.replace(/\./g, " ");
+
+ if (fieldCategory !== "general") {
+ fieldName = fieldName.split(" ").slice(1).join(" ");
+ }
- if (fieldCategory !== "general") {
- fieldName = fieldName.split(' ').slice(1).join(' ');
- }
+ const uniqueFieldIdentifier = `${fieldName}-${fieldCategory}`;
- const uniqueFieldIdentifier = `${fieldName}-${fieldCategory}`;
+ if (processedFields.has(uniqueFieldIdentifier)) {
+ continue;
+ }
- if (processedFields.has(uniqueFieldIdentifier)) {
- continue;
- }
+ if (isEffectivelyEmpty(field_id, fieldValue)) {
+ emptyFields.push({
+ fieldName,
+ fieldValue,
+ fieldCategory,
+ fieldSuggestion,
+ fieldComment,
+ });
+ } else if (fieldState === "ok") {
+ acceptedFields.push({
+ fieldName,
+ fieldValue,
+ fieldCategory,
+ fieldSuggestion,
+ fieldComment,
+ });
+ } else if (fieldState === "suggestion") {
+ suggestingFields.push({
+ fieldName,
+ fieldValue,
+ fieldCategory,
+ fieldSuggestion,
+ fieldComment,
+ });
+ } else if (fieldState === "rejected") {
+ rejectedFields.push({
+ fieldName,
+ fieldValue,
+ fieldCategory,
+ fieldSuggestion,
+ fieldComment,
+ });
+ }
- if (isEffectivelyEmpty(field_id, fieldValue)) {
- emptyFields.push({ fieldName, fieldValue, fieldCategory, fieldSuggestion });
- } else if (fieldState === 'ok') {
- acceptedFields.push({ fieldName, fieldValue, fieldCategory, fieldSuggestion });
- } else if (fieldState === 'suggestion') {
- suggestingFields.push({ fieldName, fieldValue, fieldCategory, fieldSuggestion });
- } else if (fieldState === 'rejected') {
- rejectedFields.push({ fieldName, fieldValue, fieldCategory, fieldSuggestion });
+ processedFields.add(uniqueFieldIdentifier);
+ }
}
-
- processedFields.add(uniqueFieldIdentifier);
}
const categories = document.querySelectorAll(".tab-pane");
@@ -90,38 +144,71 @@ export function renderSummaryPageFields() {
const category_fields = category.querySelectorAll(".field");
for (let field of category_fields) {
const field_id = field.id.slice(6);
- const fieldValue = $(field).find('.value').text().replace(/\s+/g, ' ').trim();
- const found = current_review.reviews.some((review) => review.key === field_id);
+ const fieldValue = $(field)
+ .find(".value")
+ .text()
+ .replace(/\s+/g, " ")
+ .trim();
+ const found = current_review.reviews.some(
+ (review) => review.key === field_id
+ );
const fieldState = getFieldState(field_id);
- const fieldCategory = field.getAttribute('data-category');
- const fieldSuggestion = field.querySelector('.suggestion.suggestion--highlight')?.textContent.trim() || "";
-
- let fieldName = field_id.replace(/\./g, ' ');
+ const fieldCategory = field.getAttribute("data-category");
+ const fieldSuggestion =
+ field
+ .querySelector(".suggestion.suggestion--highlight")
+ ?.textContent.trim() || "";
+
+ const fieldComment =
+ field
+ .querySelector(".suggestion--comment")
+ ?.textContent.trim() ||
+ field
+ .querySelector(".suggestion--additional-comment")
+ ?.textContent.trim() || "";
+
+ let fieldName = field_id.replace(/\./g, " ");
if (fieldCategory !== "general") {
- fieldName = fieldName.split(' ').slice(1).join(' ');
+ fieldName = fieldName.split(" ").slice(1).join(" ");
}
const uniqueFieldIdentifier = `${fieldName}-${fieldCategory}`;
if (
!found &&
- fieldState !== 'ok' &&
+ fieldState !== "ok" &&
!isEffectivelyEmpty(field_id, fieldValue) &&
!processedFields.has(uniqueFieldIdentifier)
) {
- missingFields.push({ fieldName, fieldValue, fieldCategory, fieldSuggestion });
+ missingFields.push({
+ fieldName,
+ fieldValue,
+ fieldCategory,
+ fieldSuggestion,
+ fieldComment,
+ });
processedFields.add(uniqueFieldIdentifier);
}
}
}
const allData = [];
- allData.push(...missingFields.map((item) => ({ ...item, fieldStatus: 'Missing' })));
- allData.push(...acceptedFields.map((item) => ({ ...item, fieldStatus: 'Accepted' })));
- allData.push(...suggestingFields.map((item) => ({ ...item, fieldStatus: 'Suggested' })));
- allData.push(...rejectedFields.map((item) => ({ ...item, fieldStatus: 'Rejected' })));
- allData.push(...emptyFields.map((item) => ({ ...item, fieldStatus: 'Empty' })));
+ allData.push(
+ ...missingFields.map((item) => ({ ...item, fieldStatus: "Missing" }))
+ );
+ allData.push(
+ ...acceptedFields.map((item) => ({ ...item, fieldStatus: "Accepted" }))
+ );
+ allData.push(
+ ...suggestingFields.map((item) => ({ ...item, fieldStatus: "Suggested" }))
+ );
+ allData.push(
+ ...rejectedFields.map((item) => ({ ...item, fieldStatus: "Rejected" }))
+ );
+ allData.push(
+ ...emptyFields.map((item) => ({ ...item, fieldStatus: "Empty" }))
+ );
const categoriesMap = {};
@@ -130,67 +217,77 @@ export function renderSummaryPageFields() {
categoriesMap[category].push(field);
}
- allData.forEach(item => {
- const category = item.fieldCategory || 'general';
+ allData.forEach((item) => {
+ const category = item.fieldCategory || "general";
addFieldToCategory(category, item);
});
const summaryContainer = document.getElementById("summary");
- summaryContainer.innerHTML = '';
+ summaryContainer.innerHTML = "";
- const tabsNav = document.createElement('ul');
- tabsNav.className = 'nav nav-tabs';
+ const tabsNav = document.createElement("ul");
+ tabsNav.className = "nav nav-tabs";
- const tabsContent = document.createElement('div');
- tabsContent.className = 'tab-content';
+ const tabsContent = document.createElement("div");
+ tabsContent.className = "tab-content";
let firstTab = true;
for (const category in categoriesMap) {
const tabId = `tab-${category}`;
- const navItem = document.createElement('li');
- navItem.className = 'nav-item';
+ const navItem = document.createElement("li");
+ navItem.className = "nav-item";
navItem.innerHTML = `
-
{% endfor %}
@@ -323,8 +320,7 @@