Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(validation): Improve annotations and showValidation error handling #2609

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
4 changes: 2 additions & 2 deletions src/js/models/metadata/eml211/EML211.js
Original file line number Diff line number Diff line change
Expand Up @@ -1709,9 +1709,9 @@ define([
}
}

// Validate the EMLAnnotation models
// Validate the EMLAnnotation models, checking for the canonical dataset
const annotations = this.get("annotations");
const annotationErrors = annotations.validate();
const annotationErrors = annotations ? annotations.validate() : [];

if (annotationErrors?.length) {
errors.annotations = annotationErrors.filter(
Expand Down
153 changes: 91 additions & 62 deletions src/js/views/metadata/EML211EditorView.js
Original file line number Diff line number Diff line change
Expand Up @@ -1218,7 +1218,10 @@ define([
}
},

/** Show any errors that occured when trying to save changes */
/**
* Show any errors that occurred when trying to save changes
*
*/
showValidation() {
// First clear all the error messaging
this.$(".notification.error").empty();
Expand All @@ -1228,75 +1231,101 @@ define([

const errors = this.model.validationError;

errors?.forEach((errorMsg, category) => {
const categoryEls = this.$(`[data-category='${category}']`);
const dataItemRow = categoryEls.parents(".data-package-item");
if (errors && typeof errors === "object") {
Object.entries(errors).forEach(([category, errorMsg]) => {
if (typeof errorMsg === "string") {
// Handle string error messages
this.showError(category, errorMsg);
} else if (typeof errorMsg === "object") {
// Handle object error messages by iterating over leaf nodes
this.showLeafErrors(category, errorMsg);
}
});

// If this field is in a DataItemView, then delegate to that view
if (dataItemRow.length && dataItemRow.data("view")) {
dataItemRow.data("view").showValidation(category, errorMsg);
return;
}
const elsWithViews = categoryEls.filter(
(el) =>
$(el).data("view") &&
$(el).data("view").showValidation &&
!$(el).data("view").isNew,
);
if (Object.keys(errors).length) {
// Create a list of errors to display in the error message shown to the user
const errorList = `<ul>${this.getErrorListItem(errors)}</ul>`;

if (elsWithViews.length) {
elsWithViews.forEach((el) => {
$(el).data("view").showValidation();
});
} else if (categoryEls.length) {
// Show the error message
categoryEls
.filter(".notification")
.addClass("error")
.text(errorMsg);

// Add the error message to inputs
categoryEls.filter("textarea, input").addClass("error");
MetacatUI.appView.showAlert(
`Fix the errors flagged below before submitting: ${errorList}`,
"alert-error",
this.$el,
null,
{
remove: true,
},
);
}
}
},

// Get the link in the table of contents navigation
let navigationLink = this.$(
`.side-nav-item[data-category='${category}']`,
);
/**
* Log an error message for a specific category
*
* @param category - The category of the error
* @param errorMsg - The error message to display
*/
showError(category, errorMsg) {
const categoryEls = this.$(`[data-category='${category}']`);
const dataItemRow = categoryEls.parents(".data-package-item");

if (!navigationLink.length) {
const section = categoryEls.parents("[data-section]");
navigationLink = this.$(
`.side-nav-item.${$(section).attr("data-section")}`,
);
}
// If this field is in a DataItemView, then delegate to that view
if (dataItemRow.length && dataItemRow.data("view")) {
dataItemRow.data("view").showValidation(category, errorMsg);
return;
}
const elsWithViews = categoryEls.filter(
(el) =>
$(el).data("view") &&
$(el).data("view").showValidation &&
!$(el).data("view").isNew,
);

// Show the error icon in the table of contents
navigationLink
.addClass("error")
.find(".icon")
.addClass("error")
.show();

this.model.off(`change:${category}`, this.model.checkValidity);
this.model.once(`change:${category}`, this.model.checkValidity);
}, this);

if (errors) {
// Create a list of errors to display in the error message shown to
// the user
const errorList = `<ul>${this.getErrorListItem(errors)}</ul>`;

MetacatUI.appView.showAlert(
`Fix the errors flagged below before submitting: ${errorList}`,
"alert-error",
this.$el,
null,
{
remove: true,
},
if (elsWithViews.length) {
elsWithViews.forEach((el) => {
$(el).data("view").showValidation();
});
} else if (categoryEls.length) {
// Show the error message
categoryEls.filter(".notification").addClass("error").text(errorMsg);

// Add the error message to inputs
categoryEls.filter("textarea, input").addClass("error");
}

// Get the link in the table of contents navigation
let navigationLink = this.$(
`.side-nav-item[data-category='${category}']`,
);

if (!navigationLink.length) {
const section = categoryEls.parents("[data-section]");
navigationLink = this.$(
`.side-nav-item.${$(section).attr("data-section")}`,
);
}

// Show the error icon in the table of contents
navigationLink.addClass("error").find(".icon").addClass("error").show();

this.model.off(`change:${category}`, this.model.checkValidity);
this.model.once(`change:${category}`, this.model.checkValidity);
},

/**
* Recursively log the leaf errors in the error object
*
* @param category - The category of the error
* @param errorObj - The object containing the error messages
*/
showLeafErrors(category, errorObj) {
Object.entries(errorObj).forEach(([subCategory, subErrorMsg]) => {
if (typeof subErrorMsg === "string") {
this.showError(`${category}`, subErrorMsg);
} else if (typeof subErrorMsg === "object") {
this.showLeafErrors(`${subCategory}`, subErrorMsg);
}
});
},

/** @inheritdoc */
Expand Down
3 changes: 2 additions & 1 deletion test/config/tests.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@
"./js/specs/unit/models/ontologies/BioontologyOntology.spec.js",
"./js/specs/unit/models/accordion/Accordion.spec.js",
"./js/specs/unit/models/accordion/AccordionItem.spec.js",
"./js/specs/unit/views/DataItemView.spec.js"
"./js/specs/unit/views/DataItemView.spec.js",
"./js/specs/unit/views/metadata/EML211EditorView.spec.js"
],
"integration": [
"./js/specs/integration/collections/SolrResults.spec.js",
Expand Down
94 changes: 94 additions & 0 deletions test/js/specs/unit/views/metadata/EML211EditorView.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
define(["jquery", "backbone", "views/metadata/EML211EditorView"], function (
$,
Backbone,
EML211EditorView,
) {
describe("EML211EditorView", function () {
let view, model, sandbox;

beforeEach(function () {
sandbox = sinon.createSandbox();
console.log("beforeEach called");
// Create a mock model with validation errors
model = new Backbone.Model();
model.validationError = {
title: "Error in title",
abstract: "Error in abstract",
methods: {
methodSteps: "Error in step 1",
},
};

// Instantiate the view with the mock model
view = new EML211EditorView({ model: model });

// Spy on the methods that interact with the DOM
sandbox.spy(view, "showError");
sandbox.spy(view, "showLeafErrors");
});

afterEach(function () {
sandbox.restore();
});

it("should log validation errors correctly", function () {
console.log("Test: should log validation errors correctly");
// Call the showValidation method
view.showValidation();

// Assert that showError and showLeafErrors were called with the expected arguments
sinon.assert.calledWith(view.showError, "methods", "Error in step 1");
sinon.assert.calledWith(view.showLeafErrors, "methods", {
methodSteps: "Error in step 1",
});
sinon.assert.calledWith(view.showError, "title", "Error in title");
sinon.assert.calledWith(view.showError, "abstract", "Error in abstract");
});

it("should handle string error messages correctly in showError", function () {
console.log(
"Test: should handle string error messages correctly in showError",
);
// Mock the category elements
view.$ = sandbox.stub().returns({
addClass: sandbox.stub().returnsThis(),
text: sandbox.stub(),
filter: sandbox.stub().returnsThis(),
parents: sandbox.stub().returnsThis(),
data: sandbox.stub().returnsThis(),
find: sandbox.stub().returnsThis(),
show: sandbox.stub(),
});

// Call the showError method
view.showError("methodSteps.step1", "Error in step 1");

// Assert that the appropriate DOM manipulation methods were called
sinon.assert.calledWith(view.$, "[data-category='methodSteps.step1']");
});

it("should handle nested error objects correctly in showLeafErrors", function () {
console.log(
"Test: should handle nested error objects correctly in showLeafErrors",
);

// Call the showLeafErrors method
view.showLeafErrors("methodSteps.step2", {
subStep1: "Error in sub-step 1",
subStep2: "Error in sub-step 2",
});

// Assert that showError was called with the expected arguments
sinon.assert.calledWith(
view.showError,
"methodSteps.step2",
"Error in sub-step 1",
);
sinon.assert.calledWith(
view.showError,
"methodSteps.step2",
"Error in sub-step 2",
);
});
});
});
Loading