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

UX: add section feedback messages #1957

Open
wants to merge 2 commits into
base: master
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
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
FILE_UPLOAD_SAVE_DRAFT_FAILED,
RESERVE_PID_FAILED,
} from "../state/types";
import { leafTraverse } from "../utils";
import PropTypes from "prop-types";

const defaultLabels = {
Expand Down Expand Up @@ -60,7 +59,7 @@
},
[DRAFT_HAS_VALIDATION_ERRORS]: {
feedback: "warning",
message: i18next.t("Record saved with validation errors:"),
message: i18next.t("Record saved with validation errors in"),
},
[DRAFT_SAVE_FAILED]: {
feedback: "negative",
Expand All @@ -77,7 +76,7 @@
[DRAFT_PUBLISH_FAILED_WITH_VALIDATION_ERRORS]: {
feedback: "negative",
message: i18next.t(
"The draft was not published. Record saved with validation errors:"
"The draft was not published. Record saved with validation errors in"
),
},
[DRAFT_SUBMIT_REVIEW_FAILED]: {
Expand All @@ -89,7 +88,7 @@
[DRAFT_SUBMIT_REVIEW_FAILED_WITH_VALIDATION_ERRORS]: {
feedback: "negative",
message: i18next.t(
"The draft was not submitted for review. Record saved with validation errors:"
"The draft was not submitted for review. Record saved with validation errors in"
),
},
[DRAFT_DELETE_FAILED]: {
Expand Down Expand Up @@ -179,7 +178,7 @@
let store = (l) => {
messages.push(l);
};
leafTraverse(errorValue, store);

Check failure on line 181 in invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/errors/FormFeedback.js

View workflow job for this annotation

GitHub Actions / JS / Tests (18.x)

'leafTraverse' is not defined

Check failure on line 181 in invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/errors/FormFeedback.js

View workflow job for this annotation

GitHub Actions / JS / Tests (20.x)

'leafTraverse' is not defined
return messages;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,324 @@
// This file is part of Invenio-RDM-Records
// Copyright (C) 2020-2023 CERN.
// Copyright (C) 2020-2022 Northwestern University.
// Copyright (C) 2021 Graz University of Technology.
//
// Invenio-RDM-Records is free software; you can redistribute it and/or modify it
// under the terms of the MIT License; see LICENSE file for more details.

import { i18next } from "@translations/invenio_rdm_records/i18next";
import _get from "lodash/get";
import _set from "lodash/set";
import _isObject from "lodash/isObject";
import _isEmpty from "lodash/isEmpty";
import React, { Component } from "react";
import { connect } from "react-redux";
import { Grid, Message } from "semantic-ui-react";
import {
DISCARD_PID_FAILED,
DRAFT_DELETE_FAILED,
DRAFT_HAS_VALIDATION_ERRORS,
DRAFT_PREVIEW_FAILED,
DRAFT_PUBLISH_FAILED,
DRAFT_PUBLISH_FAILED_WITH_VALIDATION_ERRORS,
DRAFT_SAVE_FAILED,
DRAFT_SAVE_SUCCEEDED,
DRAFT_SUBMIT_REVIEW_FAILED,
DRAFT_SUBMIT_REVIEW_FAILED_WITH_VALIDATION_ERRORS,
FILE_IMPORT_FAILED,
FILE_UPLOAD_SAVE_DRAFT_FAILED,
RESERVE_PID_FAILED,
} from "../state/types";
import { leafTraverse } from "../utils";
import PropTypes from "prop-types";

const defaultLabels = {
"files.enabled": i18next.t("Files"),
"metadata.resource_type": i18next.t("Resource type"),
"metadata.title": i18next.t("Title"),
"metadata.additional_titles": i18next.t("Additional titles"),
"metadata.publication_date": i18next.t("Publication date"),
"metadata.creators": i18next.t("Creators"),
"metadata.contributors": i18next.t("Contributors"),
"metadata.description": i18next.t("Description"),
"metadata.additional_descriptions": i18next.t("Additional descriptions"),
"metadata.rights": i18next.t("Licenses"),
"metadata.languages": i18next.t("Languages"),
"metadata.dates": i18next.t("Dates"),
"metadata.version": i18next.t("Version"),
"metadata.publisher": i18next.t("Publisher"),
"metadata.related_identifiers": i18next.t("Related works"),
"metadata.references": i18next.t("References"),
"metadata.identifiers": i18next.t("Alternate identifiers"),
"metadata.subjects": i18next.t("Keywords and subjects"),
"access.embargo.until": i18next.t("Embargo until"),
"pids.doi": i18next.t("DOI"),
};

const ACTIONS = {
[DRAFT_SAVE_SUCCEEDED]: {
feedback: "positive",
message: i18next.t("Record successfully saved."),
},
[DRAFT_HAS_VALIDATION_ERRORS]: {
feedback: "warning",
message: i18next.t("Record saved with validation errors in"),
},
[DRAFT_SAVE_FAILED]: {
feedback: "negative",
message: i18next.t(
"The draft was not saved. Please try again. If the problem persists, contact user support."
),
},
[DRAFT_PUBLISH_FAILED]: {
feedback: "negative",
message: i18next.t(
"The draft was not published. Please try again. If the problem persists, contact user support."
),
},
[DRAFT_PUBLISH_FAILED_WITH_VALIDATION_ERRORS]: {
feedback: "negative",
message: i18next.t(
"The draft was not published. Record saved with validation errors:"
),
},
[DRAFT_SUBMIT_REVIEW_FAILED]: {
feedback: "negative",
message: i18next.t(
"The draft was not submitted for review. Please try again. If the problem persists, contact user support."
),
},
[DRAFT_SUBMIT_REVIEW_FAILED_WITH_VALIDATION_ERRORS]: {
feedback: "negative",
message: i18next.t(
"The draft was not submitted for review. Record saved with validation errors:"
),
},
[DRAFT_DELETE_FAILED]: {
feedback: "negative",
message: i18next.t(
"Draft deletion failed. Please try again. If the problem persists, contact user support."
),
},
[DRAFT_PREVIEW_FAILED]: {
feedback: "negative",
message: i18next.t(
"Draft preview failed. Please try again. If the problem persists, contact user support."
),
},
[RESERVE_PID_FAILED]: {
feedback: "negative",
message: i18next.t(
"Identifier reservation failed. Please try again. If the problem persists, contact user support."
),
},
[DISCARD_PID_FAILED]: {
feedback: "negative",
message: i18next.t(
"Identifier could not be discarded. Please try again. If the problem persists, contact user support."
),
},
[FILE_UPLOAD_SAVE_DRAFT_FAILED]: {
feedback: "negative",
message: i18next.t(
"Draft save failed before file upload. Please try again. If the problem persists, contact user support."
),
},
[FILE_IMPORT_FAILED]: {
feedback: "negative",
message: i18next.t(
"Files import from the previous version failed. Please try again. If the problem persists, contact user support."
),
},
};

class DisconnectedFormFeedback extends Component {
constructor(props) {
super(props);
this.labels = {
...defaultLabels,
...props.labels,
};
}

filterMessages(errors, includesPaths = []) {
const output = {};
for (const errorPath in errors) {
for (const subPath in errors[errorPath]) {
const path = `${errorPath}.${subPath}`;
if (includesPaths.includes(path))
_set(output, path, errors[errorPath][subPath]);
}
}
return output;
}

/**
* Render error messages inline (if 1) or as list (if multiple).
*
* @param {Array<String>} messages
* @returns String or React node
*/
renderErrorMessages(messages) {
const uniqueMessages = [...new Set(messages)];
if (uniqueMessages.length === 1) {
return messages[0];
} else {
return (
<ul>
{uniqueMessages.map((m) => (
<li key={m}>{m}</li>
))}
</ul>
);
}
}

/**
* Return array of error messages from errorValue object.
*
* The error message(s) might be deeply nested in the errorValue e.g.
*
* errorValue = [
* {
* title: "Missing value"
* }
* ];
*
* @param {object} errorValue
* @returns array of Strings (error messages)
*/
toErrorMessages(errorValue) {
let messages = [];
let store = (l) => {
messages.push(l);
};
leafTraverse(errorValue, store);
return messages;
}

/**
* Return object with human readbable labels as keys and error messages as
* values given an errors object.
*
* @param {object} errors
* @returns object
*/
toLabelledErrorMessages(errors) {
// Step 0 - Create object with collapsed 1st and 2nd level keys
// e.g., {metadata: {creators: ,,,}} => {"metadata.creators": ...}
// For now, only for metadata, files and access.embargo
const metadata = errors.metadata || {};
const step0Metadata = Object.entries(metadata).map(([key, value]) => {
return ["metadata." + key, value];
});
const files = errors.files || {};
const step0Files = Object.entries(files).map(([key, value]) => {
return ["files." + key, value];
});
const access = errors.access?.embargo || {};
const step0Access = Object.entries(access).map(([key, value]) => {
return ["access.embargo." + key, value];
});
const pids = errors.pids || {};
const step0Pids = _isObject(pids)
? Object.entries(pids).map(([key, value]) => {
return ["pids." + key, value];
})
: [["pids", pids]];
const customFields = errors.custom_fields || {};
const step0CustomFields = Object.entries(customFields).map(([key, value]) => {
return ["custom_fields." + key, value];
});
const step0 = Object.fromEntries(
step0Metadata
.concat(step0Files)
.concat(step0Access)
.concat(step0Pids)
.concat(step0CustomFields)
);

// Step 1 - Transform each error value into array of error messages
const step1 = Object.fromEntries(
Object.entries(step0).map(([key, value]) => {
return [key, this.toErrorMessages(value)];
})
);

// Step 2 - Group error messages by label
// (different error keys can map to same label e.g. title and
// additional_titles)
const labelledErrorMessages = {};
for (const key in step1) {
const label = this.labels[key] || "Unknown field";
let messages = labelledErrorMessages[label] || [];
labelledErrorMessages[label] = messages.concat(step1[key]);
}

return labelledErrorMessages;
}

render() {
const { errors: errorsProp, actionState, includesPaths } = this.props;

const errors = errorsProp || {};

const { feedback, message } = _get(ACTIONS, actionState, {

Check warning on line 266 in invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/errors/FormSectionFeedback.js

View workflow job for this annotation

GitHub Actions / JS / Tests (18.x)

'message' is assigned a value but never used

Check warning on line 266 in invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/errors/FormSectionFeedback.js

View workflow job for this annotation

GitHub Actions / JS / Tests (20.x)

'message' is assigned a value but never used
feedback: undefined,
message: undefined,
});

const filteredErrors = this.filterMessages(errors, includesPaths);

if (_isEmpty(filteredErrors)) {
// if no message to display, simply return null
return null;
}

const labelledMessages = this.toLabelledErrorMessages(filteredErrors);
const listErrors = Object.entries(labelledMessages).map(([label, messages]) => (
<Message.Item key={label}>
<b>{label}</b>: {this.renderErrorMessages(messages)}
</Message.Item>
));

return (
<Message
visible
positive={feedback === "positive"}
warning={feedback === "warning"}
negative={feedback === "negative"}
className="mt-0 p-0"
>
<Grid container>
<Grid.Column width={15} textAlign="left">
{listErrors.length > 0 && <Message.List>{listErrors}</Message.List>}
</Grid.Column>
</Grid>
</Message>
);
}
}

DisconnectedFormFeedback.propTypes = {
errors: PropTypes.object,
actionState: PropTypes.string,
labels: PropTypes.object,
includesPaths: PropTypes.array.isRequired,
};

DisconnectedFormFeedback.defaultProps = {
errors: undefined,
actionState: undefined,
labels: undefined,
};

const mapStateToProps = (state) => ({
actionState: state.deposit.actionState,
errors: state.deposit.errors,
});

export const FormSectionFeedback = connect(
mapStateToProps,
null
)(DisconnectedFormFeedback);
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
// under the terms of the MIT License; see LICENSE file for more details.

export { FormFeedback } from "./FormFeedback";
export { FormSectionFeedback } from "./FormSectionFeedback";
export { DepositErrorHandler } from "./DepositErrorHandler";
Loading