diff --git a/app/assets/stylesheets/scss/_buttons.scss b/app/assets/stylesheets/scss/_buttons.scss
index 276c084bfe..fb92e10965 100644
--- a/app/assets/stylesheets/scss/_buttons.scss
+++ b/app/assets/stylesheets/scss/_buttons.scss
@@ -101,7 +101,12 @@ input[type=file]::file-selector-button {
@extend %o-button__link-style;
padding: 5px 0;
color: $dark-blue;
- font-size: 0.9em;
+ font-size: 0.98rem;
+}
+
+.o-button__plain-textlink {
+ @extend %o-button__link-style;
+ color: $dark-blue;
}
.o-button__plain-text6 {
diff --git a/app/assets/stylesheets/scss/_drop.scss b/app/assets/stylesheets/scss/_drop.scss
index dd7fb9e806..5c2117dedd 100644
--- a/app/assets/stylesheets/scss/_drop.scss
+++ b/app/assets/stylesheets/scss/_drop.scss
@@ -25,9 +25,12 @@
.dd-list-item {
list-style: "";
margin: 1rem auto;
- padding: 1rem;
+ padding: 1rem;
background-color: $lightest-blue;
- text-align: left;
+ display: flex;
+ align-items: flex-start;
+ flex-wrap: wrap;
+ gap: 1.5ch;
&.dragon-inactive {
}
@@ -36,7 +39,6 @@
@extend %o-button__link-style;
cursor: ns-resize;
float: left;
- margin-right: 1.5ch;
margin-top: 28px;
&:before {
font-family: 'Font Awesome 6 Free';
diff --git a/app/assets/stylesheets/scss/_input.scss b/app/assets/stylesheets/scss/_input.scss
index 80229daec1..284c8cc93f 100644
--- a/app/assets/stylesheets/scss/_input.scss
+++ b/app/assets/stylesheets/scss/_input.scss
@@ -15,13 +15,40 @@ input {
flex-wrap: wrap;
column-gap: 2ch;
row-gap: 1ch;
+
+ &.spaced {
+ justify-content: space-between;
+ align-items: baseline;
+ }
}
.input-label {
color: $dark-navy;
}
-label.optional:after, label span.details {
+.input-example, [id$='-ex'] {
+ font-size: .98rem;
+ color: $medium-gray;
+ margin-top: .35ch;
+ i:not(.fas, .far) {
+ &:before {
+ font-style: normal;
+ display: inline-block;
+ content: 'e.g.';
+ color: $dark-gray;
+ margin-right: .5ch;
+ }
+ &.hint:before {
+ content: 'Hint:';
+ }
+ &.ie:before {
+ content: 'i.e.'
+ }
+ }
+}
+
+label.optional:after,
+label span.details {
display: inline-block;
font-size: .98rem;
font-weight: normal;
diff --git a/app/assets/stylesheets/scss/_keywords.scss b/app/assets/stylesheets/scss/_keywords.scss
index 71acec6f1e..5f967abd03 100644
--- a/app/assets/stylesheets/scss/_keywords.scss
+++ b/app/assets/stylesheets/scss/_keywords.scss
@@ -37,7 +37,9 @@
.c-keywords__keyword-remove {
border: none;
background-color: $medium-green;
+ border: thin solid $light-green;
color: white;
+ font-size: .98rem;
line-height: 0;
&:hover, &:active, &:focus {
background-color: $dark-blue;
diff --git a/app/assets/stylesheets/scss/_steps.scss b/app/assets/stylesheets/scss/_steps.scss
index c617fec30c..ce01eb2b8a 100644
--- a/app/assets/stylesheets/scss/_steps.scss
+++ b/app/assets/stylesheets/scss/_steps.scss
@@ -9,6 +9,10 @@
flex-direction: column;
align-items: center;
flex: 1;
+ cursor: pointer;
+ &:hover .step-name {
+ text-decoration: underline;
+ }
&::after {
position: absolute;
diff --git a/app/assets/stylesheets/scss/_submission-help.scss b/app/assets/stylesheets/scss/_submission-help.scss
index 60fd359b1c..2bc8dcf70b 100644
--- a/app/assets/stylesheets/scss/_submission-help.scss
+++ b/app/assets/stylesheets/scss/_submission-help.scss
@@ -1,3 +1,40 @@
+#submission-help {
+ //position: sticky;
+ //bottom: 0;
+ //box-shadow: $lightest-green 0px -4px 4px -6px;
+ border-top: thick solid $lightest-green;
+ background-color: white;
+ padding: 2rem;
+ padding-left: 30px;
+ width: 100%;
+ display: flex;
+ gap: 2ch;
+ align-items: flex-start;
+ justify-content: space-between;
+ flex-direction: row-reverse;
+
+ #submission-help-text {
+ font-size: 1rem;
+ *:first-child {
+ margin-top: 0;
+ }
+ *:last-child {
+ margin-top: 0;
+ }
+ }
+
+ @media (max-width: 811px){
+ flex-direction: column-reverse;
+ *:first-child {
+ margin-left: auto;
+ }
+ }
+
+ button {
+ white-space: nowrap;
+ }
+}
+
#infographic {
margin: 2ch auto;
display: grid;
diff --git a/app/assets/stylesheets/scss/_submission.scss b/app/assets/stylesheets/scss/_submission.scss
index 9bca27d5c4..c946e756a0 100644
--- a/app/assets/stylesheets/scss/_submission.scss
+++ b/app/assets/stylesheets/scss/_submission.scss
@@ -503,45 +503,22 @@ dialog#submission-step[open] {
h1 {
margin-bottom: 0;
}
- & + h2 {
+ & + h2, & + div > h2:first-child {
margin-top: 1.1rem;
}
}
-#submission-help {
- //position: sticky;
- //bottom: 0;
- //box-shadow: $lightest-green 0px -4px 4px -6px;
- border-top: thick solid $lightest-green;
- background-color: white;
- padding: 2rem;
- padding-left: 30px;
- width: 100%;
+.drag-instruct {
+ font-size: .98rem;
display: flex;
+ align-items: baseline;
gap: 2ch;
- align-items: flex-start;
- justify-content: space-between;
- flex-direction: row-reverse;
-
- #submission-help-text {
- font-size: 1rem;
- *:first-child {
- margin-top: 0;
- }
- *:last-child {
- margin-top: 0;
- }
- }
-
- @media (max-width: 811px){
- flex-direction: column-reverse;
- *:first-child {
- margin-left: auto;
- }
+ flex-wrap: wrap;
+ i {
+ margin: 0 .5ch;
}
-
- button {
- white-space: nowrap;
+ & > * {
+ margin-bottom: 0;
}
}
@@ -571,52 +548,95 @@ dialog#submission-step[open] {
flex-wrap: wrap;
column-gap: 1.5ch;
row-gap: 1ch;
+ flex: 1;
- & > span {
- display: block;
- align-self: start;
- }
-
- .remove-record {
+ .remove-record, & + .remove-record {
@extend %o-button__link-style;
margin-top: 28px;
}
}
+%author-form-button {
+ color: $dark-blue;
+ background-color: white;
+ border: thin solid $medium-blue;
+ padding: 2px 5px;
+ &:hover, &:active, &:focus {
+ background-color: $dark-blue;
+ color: white;
+ }
+}
+
.author-form > div {
- flex-basis: 25%;
- max-width: calc(25% - 2.2ch);
+ flex-basis: calc(33% - 1ch);
+ flex-shrink: 1;
+ flex-grow: 0;
+ max-width: 100%;
+ min-width: 165px;
* {
max-width: 100%;
text-overflow: ellipsis;
}
+ &.affiliation-input {
+ flex: 1;
+ min-width: 200px;
+
+ .input-line {
+ align-items: baseline;
+ gap: 1ch;
+ flex-wrap: nowrap;
+ button {
+ @extend %author-form-button;
+ line-height: 0;
+ font-size: .9rem;
+ }
+ }
+
+ & + span {
+ display: block;
+ align-self: start;
+ button {
+ @extend %author-form-button;
+ margin-top: 28px;
+ font-size: .98rem;
+ padding-bottom: 0;
+ }
+ }
+ }
@media (max-width: $screen-lg-min) {
- flex-basis: 50%;
- max-width: calc(50% - 2.2ch);
+ flex-basis: calc(50% - 1ch);
}
- @media (max-width: 600px) {
+ @media (max-width: 520px) {
flex-basis: 100%;
- max-width: 100%;
+ }
+}
+
+.open .author-form > div {
+ @media (max-width: 740px) {
+ flex-basis: 100%;
+ }
+}
+
+.auth-buttons {
+ display: flex;
+ flex-direction: row-reverse;
+ align-items: center;
+
+ i {
+ transform: rotate(75deg);
+ color: $dark-gray;
}
}
.funder-form {
padding-bottom: 1ch;
& > div {
- flex-basis: 33%;
- max-width: calc(33% - 2.2ch);
+ flex-basis: 25%;
+ flex: 1;
* {
max-width: 100%;
text-overflow: ellipsis;
}
- @media (max-width: 800px) {
- flex-basis: 50%;
- max-width: calc(50% - 2.2ch);
- }
- @media (max-width: 600px) {
- flex-basis: 100%;
- max-width: 100%;
- }
}
}
diff --git a/app/assets/stylesheets/scss/_upload-table.scss b/app/assets/stylesheets/scss/_upload-table.scss
index ce9cfc9dff..f6c82c7a4c 100644
--- a/app/assets/stylesheets/scss/_upload-table.scss
+++ b/app/assets/stylesheets/scss/_upload-table.scss
@@ -16,10 +16,11 @@
.c-uploadtable {
min-width: 100%;
+ font-size: 1rem;
th,
td {
- padding: $spacing-sm;
+ padding: .75rem;
border: thin solid #888;
text-align: left;
}
diff --git a/app/assets/stylesheets/scss/_variables.scss b/app/assets/stylesheets/scss/_variables.scss
index 922961ded0..bd1cfbd523 100644
--- a/app/assets/stylesheets/scss/_variables.scss
+++ b/app/assets/stylesheets/scss/_variables.scss
@@ -15,6 +15,7 @@ $light-blue: rgb(55, 150, 196);
$lighter-blue: lighten($light-blue, 25%);
$lightest-blue: lighten($light-blue, 44%);
$dark-gray: rgb(78, 80, 80);
+$medium-gray: #767676;
$light-gray: rgb(157, 159, 162);
$lighter-gray: rgb(211, 211, 211);
$lightest-gray: rgb(235, 235, 235);
diff --git a/app/controllers/stash_datacite/authors_controller.rb b/app/controllers/stash_datacite/authors_controller.rb
index d8a4952e9b..0f931f93e3 100644
--- a/app/controllers/stash_datacite/authors_controller.rb
+++ b/app/controllers/stash_datacite/authors_controller.rb
@@ -16,7 +16,6 @@ def new
def create
respond_to do |format|
@author = StashEngine::Author.create(author_params)
- process_affiliation unless params[:affiliation].nil?
@author.reload
format.js
format.json { render json: @author.as_json(include: :affiliations) }
@@ -27,7 +26,7 @@ def create
def update
respond_to do |format|
@author.update(author_params)
- process_affiliation
+ process_affiliations
format.js { render template: 'stash_datacite/shared/update.js.erb' }
format.json { render json: @author.as_json(include: :affiliations) }
end
@@ -75,29 +74,38 @@ def set_author
# Only allow a trusted parameter "white list" through.
def author_params
- params.require(:author).permit(:id, :author_first_name, :author_last_name, :author_middle_name,
- :author_email, :resource_id, :author_orcid, :author_order,
+ params.require(:author).permit(:id, :author_first_name, :author_last_name, :author_org_name,
+ :author_email, :resource_id, :author_orcid, :author_order, :corresp,
affiliation: %i[id ror_id long_name])
end
+ def aff_params
+ params.require(:author).permit(:id, :author_first_name, :author_last_name, :author_org_name,
+ :author_email, :resource_id, :author_orcid, :author_order, :corresp,
+ affiliations: %i[id ror_id long_name])
+ end
+
def check_for_orcid(author)
author&.author_orcid ? true : false
end
# find correct affiliation based on long_name and ror_id and set it, create one if needed.
- def process_affiliation
+ def process_affiliations
return nil unless @author.present?
- args = author_params
- if args['affiliation']['long_name'].blank?
- @author.affiliations.destroy_all
- return
+ @author.affiliations.destroy_all
+ args = aff_params
+ affs = args['affiliations']&.reject { |a| a['long_name'].blank? }
+ affs.each do |aff|
+ process_affiliation(aff['long_name'], aff['ror_id'])
end
+ end
+
+ def process_affiliation(name, ror_val)
+ return nil unless @author.present?
# find a matching pre-existing affiliation
affil = nil
- name = args['affiliation']['long_name']
- ror_val = args['affiliation']['ror_id']
if ror_val.present?
# - find by ror_id if avaialable
affil = StashDatacite::Affiliation.where(ror_id: ror_val).first
@@ -115,10 +123,9 @@ def process_affiliation
StashDatacite::Affiliation.create(long_name: name.to_s, ror_id: nil)
end
end
- return if @author.affiliation == affil
+ return if @author.affiliations.pluck(:ror_id).include?(affil.ror_id)
@author.affiliation = affil
- @author.save
end
def check_reorder_valid
diff --git a/app/controllers/stash_engine/resources_controller.rb b/app/controllers/stash_engine/resources_controller.rb
index da03858a4b..0209389a5d 100644
--- a/app/controllers/stash_engine/resources_controller.rb
+++ b/app/controllers/stash_engine/resources_controller.rb
@@ -174,20 +174,20 @@ def display_collection
end
def dupe_check
- dupes = nil
+ dupes = []
if @resource.title && @resource.title.length > 3
other_submissions = params.key?(:admin) ? StashEngine::Resources.all : @resource.user.resources
other_submissions = other_submissions.latest_per_dataset.where.not(identifier_id: @resource.identifier_id)
primary_article = @resource.related_identifiers.find_by(work_type: 'primary_article')&.related_identifier
manuscript = @resource.resource_publication.manuscript_number
- dupes = other_submissions.where(title: @resource.title).select(:id, :title).to_a
+ dupes = other_submissions.where(title: @resource.title)&.select(:id, :title).to_a
if primary_article.present?
dupes.concat(other_submissions.joins(:related_identifiers)
- .where(related_identifiers: { work_type: 'primary_article', related_identifier: primary_article }).select(:id, :title).to_a)
+ .where(related_identifiers: { work_type: 'primary_article', related_identifier: primary_article })&.select(:id, :title).to_a)
end
if manuscript.present?
dupes.concat(
- other_submissions.joins(:resource_publication).find_by(resource_publication: { manuscript_number: manuscript }).select(:id, :title).to_a
+ other_submissions.joins(:resource_publication).find_by(resource_publication: { manuscript_number: manuscript })&.select(:id, :title).to_a
)
end
end
diff --git a/app/javascript/react/components/MarkdownEditor/CodeEditor.jsx b/app/javascript/react/components/MarkdownEditor/CodeEditor.jsx
index 4cfbd04013..b73d8cbf3c 100644
--- a/app/javascript/react/components/MarkdownEditor/CodeEditor.jsx
+++ b/app/javascript/react/components/MarkdownEditor/CodeEditor.jsx
@@ -105,6 +105,10 @@ export default function CodeEditor({
base: markdownLanguage,
extensions: [markdownTags],
}),
+ EditorView.contentAttributes.of({
+ 'aria-labelledby': 'md_editor_label',
+ 'aria-errormessage': 'readme_error',
+ }),
EditorView.updateListener.of((v) => {
if (v.docChanged) {
onChange(v.state.doc.toString());
diff --git a/app/javascript/react/components/MarkdownEditor/milkdownConfig.jsx b/app/javascript/react/components/MarkdownEditor/milkdownConfig.jsx
index 3bb0d8e787..10111c05a1 100644
--- a/app/javascript/react/components/MarkdownEditor/milkdownConfig.jsx
+++ b/app/javascript/react/components/MarkdownEditor/milkdownConfig.jsx
@@ -9,6 +9,7 @@ const dryadConfig = (ctx) => {
attributes: {
class: 'milkdown dryad-milkdown-theme',
'aria-errormessage': 'readme_error',
+ 'aria-labelledby': 'md_editor_label',
},
}));
};
diff --git a/app/javascript/react/components/MarkdownEditor/milkdown_editor.css b/app/javascript/react/components/MarkdownEditor/milkdown_editor.css
index d4f7e1dafb..f52c5efc68 100644
--- a/app/javascript/react/components/MarkdownEditor/milkdown_editor.css
+++ b/app/javascript/react/components/MarkdownEditor/milkdown_editor.css
@@ -15,6 +15,7 @@
height: 55vh;
position: relative;
overflow: auto;
+ background-color: white;
}
#readme_step_editor .md_editor_textarea {
diff --git a/app/javascript/react/components/MetadataEntry/Authors/Affiliations.jsx b/app/javascript/react/components/MetadataEntry/Authors/Affiliations.jsx
new file mode 100644
index 0000000000..0fb7b8c4b4
--- /dev/null
+++ b/app/javascript/react/components/MetadataEntry/Authors/Affiliations.jsx
@@ -0,0 +1,62 @@
+import React, {useState, useEffect} from 'react';
+import RorAutocomplete from '../RorAutocomplete';
+/* eslint-disable react/no-array-index-key */
+
+export default function Affiliations({
+ formRef, id, affiliations, setAffiliations,
+}) {
+ const [affs, setAffs] = useState(affiliations.length > 0 ? affiliations : [{long_name: '', ror_id: ''}]);
+
+ useEffect(() => {
+ setAffiliations(affs);
+ }, [affs]);
+
+ const updateName = (i, v) => {
+ setAffs((afs) => afs.map((a, x) => (i === x ? {...a, long_name: v} : a)));
+ };
+ const updateID = (i, v) => {
+ setAffs((afs) => afs.map((a, x) => (i === x ? {...a, ror_id: v} : a)));
+ };
+ const newAff = (e) => {
+ setAffs((afs) => afs.concat([{long_name: '', ror_id: ''}]));
+ e.target.blur();
+ };
+ const removeAff = (i) => {
+ affs.splice(i, 1);
+ setAffs(affs);
+ };
+
+ return (
+ <>
+ {affs.map((aff, i) => (
+
+
+
+ Institutional affiliation
+
+ {i !== 0 && (
+ removeAff(i)}>
+
+
+ )}
+
+
updateName(i, v)}
+ acID={aff.ror_id}
+ setAcID={(v) => updateID(i, v)}
+ controlOptions={{
+ htmlId: `instit_affil_${id}-${i}`,
+ isRequired: true,
+ errorId: 'author_aff_error',
+ desBy: `${id}-${`aff${i}`}-ex`,
+ }}
+ />
+ Employer or sponsor
+
+ ))}
+ + Add affiliation
+ >
+ );
+}
diff --git a/app/javascript/react/components/MetadataEntry/Authors/AuthPreview.jsx b/app/javascript/react/components/MetadataEntry/Authors/AuthPreview.jsx
index 736dae73b8..7001d83777 100644
--- a/app/javascript/react/components/MetadataEntry/Authors/AuthPreview.jsx
+++ b/app/javascript/react/components/MetadataEntry/Authors/AuthPreview.jsx
@@ -9,12 +9,15 @@ const affName = (aff) => {
};
const getAffs = (ar) => ar.map((a) => a.affiliations).flat().reduce((arr, aff) => {
- if (affName(aff)) arr.push([aff.id, affName(aff), aff.ror_id]);
+ if (affName(aff) && !arr.some((c) => aff.id === c[0])) arr.push([aff.id, affName(aff), aff.ror_id]);
return arr;
}, []);
const fullname = (author) => [author?.author_first_name, author?.author_last_name].filter(Boolean).join(' ');
-const citename = (author) => [author?.author_last_name, author?.author_first_name].filter(Boolean).join(', ');
+const citename = (author) => {
+ if (author?.author_org_name) return author.author_org_name;
+ return [author?.author_last_name, author?.author_first_name].filter(Boolean).join(', ');
+};
export default function AuthPreview({resource, previous, admin}) {
const {authors} = resource;
@@ -56,37 +59,42 @@ export default function AuthPreview({resource, previous, admin}) {
})}
{author.author_email && (
<>
-
-
-
- {previous && author.author_email !== prev?.author_email && prev?.author_email && (
- {prev.author_email}
+ {author.corresp && (
+
+
+
+ )}
+ {admin && !author.corresp && (
+
+ {previous && author.author_email !== prev?.author_email ? {author.author_email} : author.author_email}
+
)}
>
)}
+ {previous && author.author_email !== prev?.author_email && prev?.author_email && (
+ {prev.author_email}
+ )}
{author.author_orcid && (
- <>
-
-
-
- {previous && author.author_orcid !== prev?.author_orcid && prev?.author_orcid && (
- {prev.author_orcid}
- )}
- >
+
+
+
+ )}
+ {previous && author.author_orcid !== prev?.author_orcid && prev?.author_orcid && (
+ {prev.author_orcid}
)}
{i < authors.length - 1 && '; '}
diff --git a/app/javascript/react/components/MetadataEntry/Authors/AuthorForm.jsx b/app/javascript/react/components/MetadataEntry/Authors/AuthorForm.jsx
index cde10cebbc..5032b93952 100644
--- a/app/javascript/react/components/MetadataEntry/Authors/AuthorForm.jsx
+++ b/app/javascript/react/components/MetadataEntry/Authors/AuthorForm.jsx
@@ -1,55 +1,33 @@
import React, {useState, useRef} from 'react';
import {Field, Form, Formik} from 'formik';
-import axios from 'axios';
import PropTypes from 'prop-types';
-import {showModalYNDialog, showSavedMsg, showSavingMsg} from '../../../../lib/utils';
-import RorAutocomplete from '../RorAutocomplete';
+import Affiliations from './Affiliations';
+import OrcidInfo from './OrcidInfo';
-// author below has nested affiliation
export default function AuthorForm({
- author, update, remove, ownerId,
+ author, update, ownerId, admin,
}) {
const formRef = useRef(0);
-
- // the follow autocomplete items are lifted up state that is normally just part of the form, but doesn't work with Formik
- const [acText, setAcText] = useState(author?.affiliations[0]?.long_name || '');
- const [acID, setAcID] = useState(author?.affiliations[0]?.ror_id || '');
+ const [affiliations, setAffiliations] = useState(author?.affiliations);
const submitForm = (values) => {
- showSavingMsg();
-
- // set up values
- const csrf = document.querySelector("meta[name='csrf-token']")?.getAttribute('content');
-
- const submitVals = {
- authenticity_token: csrf,
- author: {
- id: values.id,
- author_first_name: values.author_first_name,
- author_last_name: values.author_last_name,
- author_email: values.author_email,
- resource_id: author.resource_id,
- affiliation: {long_name: acText, ror_id: acID},
- },
+ const submit = {
+ id: values.id,
+ author_first_name: values.author_first_name,
+ author_last_name: values.author_last_name,
+ author_org_name: values.author_org_name || null,
+ author_email: values.author_email,
+ resource_id: author.resource_id,
+ affiliations,
};
+ return update(submit);
+ };
- // submit by json
- return axios.patch(
- '/stash_datacite/authors/update',
- submitVals,
- {
- headers: {
- 'Content-Type': 'application/json; charset=utf-8',
- Accept: 'application/json',
- },
- },
- ).then((data) => {
- if (data.status !== 200) {
- console.log('Response failure not a 200 response from author save');
- }
- update((authors) => authors.map((a) => (a.id === values.id ? data.data : a)));
- showSavedMsg();
- });
+ const validateEmail = (value) => {
+ if (value && !/^[\w+\-.]+@[a-z\d-]+(\.[a-z\d-]+)*\.[a-z]+$/i.test(value)) {
+ return 'Invalid email address';
+ }
+ return null;
};
return (
@@ -58,95 +36,108 @@ export default function AuthorForm({
{
author_first_name: (author.author_first_name || ''),
author_last_name: (author.author_last_name || ''),
+ author_org_name: (author.author_org_name || ''),
author_email: (author.author_email || ''),
id: (author.id),
}
}
innerRef={formRef}
onSubmit={(values, {setSubmitting}) => {
- submitForm(values).then(() => { setSubmitting(false); });
+ submitForm(values);
+ setSubmitting(false);
}}
+ validateOnChange={false}
>
- {(formik) => (
+ {({handleSubmit, errors, touched}) => (
)}
@@ -157,6 +148,6 @@ export default function AuthorForm({
AuthorForm.propTypes = {
author: PropTypes.object.isRequired,
update: PropTypes.func.isRequired,
- remove: PropTypes.func.isRequired,
+ admin: PropTypes.bool.isRequired,
ownerId: PropTypes.number.isRequired,
};
diff --git a/app/javascript/react/components/MetadataEntry/Authors/Authors.jsx b/app/javascript/react/components/MetadataEntry/Authors/Authors.jsx
index 10e80f0f8c..155734986c 100644
--- a/app/javascript/react/components/MetadataEntry/Authors/Authors.jsx
+++ b/app/javascript/react/components/MetadataEntry/Authors/Authors.jsx
@@ -2,9 +2,8 @@ import React, {useState, useEffect} from 'react';
import axios from 'axios';
import PropTypes from 'prop-types';
import DragonDropList, {DragonListItem, orderedItems} from '../DragonDropList';
-import {showSavedMsg, showSavingMsg} from '../../../../lib/utils';
+import {showSavedMsg, showSavingMsg, showModalYNDialog} from '../../../../lib/utils';
import AuthorForm from './AuthorForm';
-import OrcidInfo from './OrcidInfo';
export default function Authors({
resource, setResource, admin, ownerId,
@@ -17,12 +16,14 @@ export default function Authors({
const blankAuthor = {
author_first_name: '',
author_last_name: '',
+ author_org_name: null,
author_email: '',
author_orcid: null,
resource_id: resource.id,
};
- const addNewAuthor = () => {
+ const addNewAuthor = (org) => {
+ if (org) blankAuthor.author_org_name = '';
const authorJson = {authenticity_token, author: {...blankAuthor, author_order: lastOrder()}};
axios.post(
'/stash_datacite/authors/create',
@@ -36,6 +37,21 @@ export default function Authors({
});
};
+ const updateItem = (author) => {
+ showSavingMsg();
+ return axios.patch(
+ '/stash_datacite/authors/update',
+ {authenticity_token, author},
+ {headers: {'Content-Type': 'application/json; charset=utf-8', Accept: 'application/json'}},
+ ).then((data) => {
+ if (data.status !== 200) {
+ console.log('Response failure not a 200 response from author save');
+ }
+ setAuthors((as) => as.map((a) => (a.id === author.id ? data.data : a)));
+ showSavedMsg();
+ });
+ };
+
const removeItem = (id, resource_id) => {
showSavingMsg();
const submitVals = {authenticity_token, author: {id, resource_id}};
@@ -48,7 +64,7 @@ export default function Authors({
}
showSavedMsg();
});
- setAuthors((a) => a.filter((item) => (item.id !== id)));
+ setAuthors((a) => a.filter((item) => item.id !== id));
};
useEffect(() => {
@@ -57,23 +73,41 @@ export default function Authors({
return (
<>
- Authors
+
+
Authors
+
Drag to reorder
+
{orderedItems({items: authors, typeName: 'author'}).map((author) => (
-
-
+
+ {ownerId !== author.id && (
+ {
+ showModalYNDialog('Are you sure you want to remove this author?', () => {
+ removeItem(author.id, author.resource_id);
+ // deleteItem(auth.id);
+ });
+ }}
+ aria-label="Remove author"
+ title="Remove"
+ >
+
+
+ )}
))}
-
-
+
+ addNewAuthor(false)}>
+ Add author
+
+ addNewAuthor(true)}>
+ + Add group author
+
>
);
diff --git a/app/javascript/react/components/MetadataEntry/Authors/OrcidInfo.jsx b/app/javascript/react/components/MetadataEntry/Authors/OrcidInfo.jsx
index 8036f50604..f2b33bda0c 100644
--- a/app/javascript/react/components/MetadataEntry/Authors/OrcidInfo.jsx
+++ b/app/javascript/react/components/MetadataEntry/Authors/OrcidInfo.jsx
@@ -10,7 +10,7 @@ export default function OrcidInfo({
}) {
const orcidInfo = author.author_orcid ? orcidURL(author.author_orcid) : null;
return (
-
+ <>
{orcidInfo && (
@@ -19,7 +19,7 @@ export default function OrcidInfo({
)}
{ownerId === author.id && (
- Corresponding author
+ Submitter
)}
{(curator && !orcidInfo && author.orcid_invite_path) ? (
@@ -27,7 +27,7 @@ export default function OrcidInfo({
Associate at {author.orcid_invite_path}
) : ''}
-
+ >
);
}
diff --git a/app/javascript/react/components/MetadataEntry/Authors/index.jsx b/app/javascript/react/components/MetadataEntry/Authors/index.jsx
index 5c5c58224e..b3d3b6e49c 100644
--- a/app/javascript/react/components/MetadataEntry/Authors/index.jsx
+++ b/app/javascript/react/components/MetadataEntry/Authors/index.jsx
@@ -4,6 +4,8 @@ import {upCase, ordinalNumber} from '../../../../lib/utils';
export {default} from './Authors';
export {default as AuthPreview} from './AuthPreview';
+const checkName = (a) => [a.author_first_name, a.author_last_name, a.author_org_name].filter(Boolean).join(' ').toLowerCase();
+
export const authorCheck = (authors, id) => {
if (!authors.find((a) => a.id === id)?.author_email) {
const ind = authors.findIndex((a) => a.id === id);
@@ -11,26 +13,39 @@ export const authorCheck = (authors, id) => {
Submitting author email is required
);
}
- const fnameErr = authors.findIndex((a) => !a.author_first_name);
+ const fnameErr = authors.findIndex((a) => !a.author_first_name && !a.author_org_name);
if (fnameErr >= 0) {
return (
- {upCase(ordinalNumber(fnameErr + 1))} author first name is required
+ {upCase(ordinalNumber(fnameErr + 1))} author name is required
);
}
- const lnameErr = authors.findIndex((a) => !a.author_last_name);
- if (lnameErr >= 0) {
+ const affErr = authors.findIndex((a) => !a.author_org_name && !a.affiliations?.[0]?.long_name);
+ if (affErr >= 0) {
return (
-
- {upCase(ordinalNumber(lnameErr + 1))} author last name is required
+
{upCase(ordinalNumber(affErr + 1))} author affiliation is required
+ );
+ }
+ const dupeName = authors.findIndex((a, i) => authors.find((au, x) => (i === x ? false : checkName(a) === checkName(au))));
+ if (dupeName >= 0) {
+ const name = checkName(authors[dupeName]);
+ const last = authors.findLastIndex((a) => checkName(a) === name);
+ return (
+
+ The {ordinalNumber(last + 1)} author's name is the same as the {ordinalNumber(dupeName + 1)} author. Is this a duplicate?
);
}
- const affErr = authors.findIndex((a) => !a.affiliations?.[0]?.long_name);
- if (affErr >= 0) {
+ const dupeEmail = authors.findIndex((a, i) => authors.find((au, x) => (i === x ? false
+ : a.author_email.toLowerCase() === au.author_email.toLowerCase())));
+ if (dupeEmail >= 0) {
+ const email = authors[dupeEmail].author_email;
+ const last = authors.findLastIndex((a) => a.author_email === email);
return (
- {upCase(ordinalNumber(affErr + 1))} author affiliation is required
+
+ The {ordinalNumber(last + 1)} author's email address the same as the {ordinalNumber(dupeEmail + 1)} author. Is this a duplicate?
+
);
}
return false;
diff --git a/app/javascript/react/components/MetadataEntry/Autocomplete.jsx b/app/javascript/react/components/MetadataEntry/Autocomplete.jsx
index 015e1b60e8..15c66355e0 100644
--- a/app/javascript/react/components/MetadataEntry/Autocomplete.jsx
+++ b/app/javascript/react/components/MetadataEntry/Autocomplete.jsx
@@ -29,7 +29,7 @@ export default function Autocomplete(
{
acText, setAcText, acID, setAcID, setAutoBlurred, supplyLookupList, nameFunc, idFunc,
controlOptions: {
- htmlId, labelText, isRequired, errorId, saveOnEnter, showDropdown,
+ htmlId, labelText, desBy, isRequired, errorId, saveOnEnter, showDropdown,
},
},
) {
@@ -115,6 +115,8 @@ export default function Autocomplete(
aria-controls={`menu_${htmlId}`}
className="c-auto_complete"
aria-errormessage={errorId || null}
+ aria-label={`${labelText ? `${labelText} s` : 'S'}earch + select`}
+ aria-describedby={desBy || null}
>
- {` I cannot find my ${labelText.toLowerCase()}, "${acText}", in the list`}
+ {` I cannot find my ${labelText?.toLowerCase() || 'item'}, "${acText}", in the list`}
>
)}
diff --git a/app/javascript/react/components/MetadataEntry/Description/Description.jsx b/app/javascript/react/components/MetadataEntry/Description/Description.jsx
index 77e16cfd93..5632090eb5 100644
--- a/app/javascript/react/components/MetadataEntry/Description/Description.jsx
+++ b/app/javascript/react/components/MetadataEntry/Description/Description.jsx
@@ -88,14 +88,15 @@ export default function Description({
return (
<>
-
+
{mceLabel.label}
+ {mceLabel.describe &&
{mceLabel.describe}
}
-
Press Alt 0 or Option 0 for help using the rich text editor with keyboard only.
>
);
}
diff --git a/app/javascript/react/components/MetadataEntry/Description/DescriptionGroup.jsx b/app/javascript/react/components/MetadataEntry/Description/DescriptionGroup.jsx
index 3ee5547ebf..43621bc1a6 100644
--- a/app/javascript/react/components/MetadataEntry/Description/DescriptionGroup.jsx
+++ b/app/javascript/react/components/MetadataEntry/Description/DescriptionGroup.jsx
@@ -15,16 +15,20 @@ export default function DescriptionGroup({
const [showCedar, setShowCedar] = useState(!!res.cedar_json);
const [template, setTemplate] = useState(null);
- const abstractLabel = {label: 'Abstract', required: true, describe: ''};
+ const abstractLabel = {
+ label: 'Abstract',
+ required: true,
+ describe: <>
An introductory description of your dataset>,
+ };
const methodsLabel = {
label: 'Methods',
required: false,
- describe: 'How was this dataset collected? How has it been processed?',
+ describe: <>
A description of the collection and processing of the data>,
};
const usageLabel = {
label: 'Usage notes',
required: false,
- describe: 'What programs and/or software are required to open the data files included with your submission?',
+ describe: <>
Programs and software required to open the data files>,
};
useEffect(() => {
@@ -53,7 +57,7 @@ export default function DescriptionGroup({
{openMethods ? (
) : (
-
setOpenMethods(true)}>+ Add methods section
+
setOpenMethods(true)}>+ Add methods section
)}
{usage?.description && (
diff --git a/app/javascript/react/components/MetadataEntry/Publication/Publication.jsx b/app/javascript/react/components/MetadataEntry/Publication/Publication.jsx
index 37db1e67de..d579b8cf3e 100644
--- a/app/javascript/react/components/MetadataEntry/Publication/Publication.jsx
+++ b/app/javascript/react/components/MetadataEntry/Publication/Publication.jsx
@@ -77,7 +77,7 @@ export default function Publication({resource, setResource}) {
-
+
Is your {subType === 'collection' ? 'collection associated with' : 'data used in'} a submitted manuscript?
diff --git a/app/javascript/react/components/MetadataEntry/Publication/PublicationForm.jsx b/app/javascript/react/components/MetadataEntry/Publication/PublicationForm.jsx
index c08ef0e65b..10caf1bf3d 100644
--- a/app/javascript/react/components/MetadataEntry/Publication/PublicationForm.jsx
+++ b/app/javascript/react/components/MetadataEntry/Publication/PublicationForm.jsx
@@ -135,13 +135,15 @@ function PublicationForm({
setAPIJournal={setAPIJournal}
controlOptions={
{
- htmlId: 'publication',
labelText: 'Journal name',
+ htmlId: 'publication',
isRequired: true,
errorId: 'journal_error',
+ desBy: 'journal-ex',
}
}
/>
+
Nature, Science
{importType !== 'manuscript' && (
@@ -150,7 +152,6 @@ function PublicationForm({
+
10.5702/qlm.1266rr
)}
{importType !== 'published' && (
@@ -170,7 +173,6 @@ function PublicationForm({
+ APPS-D-17-00113
)}
diff --git a/app/javascript/react/components/MetadataEntry/Publication/Title.jsx b/app/javascript/react/components/MetadataEntry/Publication/Title.jsx
index 98ac917eac..c9b33fa5cc 100644
--- a/app/javascript/react/components/MetadataEntry/Publication/Title.jsx
+++ b/app/javascript/react/components/MetadataEntry/Publication/Title.jsx
@@ -53,14 +53,12 @@ function Title({resource, setResource}) {
type="text"
className="title c-input__text"
id={`title__${resource.id}`}
- onBlur={() => { // formRef.current.handleSubmit();
- formik.handleSubmit();
- }}
+ onBlur={formik.handleSubmit}
required
+ aria-describedby="title-ex"
aria-errormessage="title_error"
/>
-
-
+
The title should be a succinct summary of the data and its purpose or use
)}
diff --git a/app/javascript/react/components/MetadataEntry/RelatedWorks/RelatedWorkForm.jsx b/app/javascript/react/components/MetadataEntry/RelatedWorks/RelatedWorkForm.jsx
index acef38d44b..2626e697c4 100644
--- a/app/javascript/react/components/MetadataEntry/RelatedWorks/RelatedWorkForm.jsx
+++ b/app/javascript/react/components/MetadataEntry/RelatedWorks/RelatedWorkForm.jsx
@@ -84,9 +84,7 @@ function RelatedWorkForm(
name="work_type"
as="select"
className="c-input__select"
- onBlur={() => { // defaults to formik.handleBlur
- formik.handleSubmit();
- }}
+ onBlur={formik.handleSubmit}
>
{workTypes.map((opt) => (
@@ -103,16 +101,14 @@ function RelatedWorkForm(
id={`related_identifier__${relatedIdentifier.id}`}
name="related_identifier"
type="text"
- size="40"
- placeholder="example: https://doi.org/10.1594/PANGAEA.726855"
aria-errormessage="works_error"
+ aria-describedby={`${relatedIdentifier.id}url-ex`}
className="c-input__text"
- onBlur={() => { // defaults to formik.handleBlur
- formik.handleSubmit();
- }}
+ onBlur={formik.handleSubmit}
/>
+ https://doi.org/10.1594/PANGAEA.726855
-
+
)}
-
+
+
+
>
);
}
diff --git a/app/javascript/react/components/MetadataEntry/RelatedWorks/RelatedWorksErrors.jsx b/app/javascript/react/components/MetadataEntry/RelatedWorks/RelatedWorksErrors.jsx
index 1dbd2dec1e..33b8ced252 100644
--- a/app/javascript/react/components/MetadataEntry/RelatedWorks/RelatedWorksErrors.jsx
+++ b/app/javascript/react/components/MetadataEntry/RelatedWorks/RelatedWorksErrors.jsx
@@ -17,14 +17,14 @@ function RelatedWorksErrors(
if (relatedIdentifier.related_identifier) {
if (!urlCheck(relatedIdentifier.related_identifier)) {
return (
-
+
The URL is not valid. Make sure your URL is correct and begins with http://
or https://
.
);
}
if (!relatedIdentifier.verified) {
return (
-
+
The web page cannot be verified. Make sure your URL is correct.
);
diff --git a/app/javascript/react/components/MetadataEntry/Subjects/Keywords.jsx b/app/javascript/react/components/MetadataEntry/Subjects/Keywords.jsx
index ac7c6981bd..17b99230a5 100644
--- a/app/javascript/react/components/MetadataEntry/Subjects/Keywords.jsx
+++ b/app/javascript/react/components/MetadataEntry/Subjects/Keywords.jsx
@@ -51,11 +51,18 @@ function Keywords({resource, setResource}) {
Subject keywords
- (at least 3 required)
+ (at least 3)
-
+
{subjs.map((subj) => (
-
+
{subj.subject}
+
Scientific names, method types, or keywords from your related article
);
}
diff --git a/app/javascript/react/components/MetadataEntry/Subjects/ResearchDomain.jsx b/app/javascript/react/components/MetadataEntry/Subjects/ResearchDomain.jsx
index 6fa28e0282..8fcf3a7d8b 100644
--- a/app/javascript/react/components/MetadataEntry/Subjects/ResearchDomain.jsx
+++ b/app/javascript/react/components/MetadataEntry/Subjects/ResearchDomain.jsx
@@ -50,6 +50,7 @@ function ResearchDomain({resource, setResource}) {
return (
);
}
diff --git a/app/javascript/react/components/MetadataEntry/Support/FacilityForm.jsx b/app/javascript/react/components/MetadataEntry/Support/FacilityForm.jsx
index 6ac125ae1a..f0a5b3925c 100644
--- a/app/javascript/react/components/MetadataEntry/Support/FacilityForm.jsx
+++ b/app/javascript/react/components/MetadataEntry/Support/FacilityForm.jsx
@@ -6,8 +6,7 @@ import RorAutocomplete from '../RorAutocomplete';
import {showSavedMsg, showSavingMsg} from '../../../../lib/utils';
export default function FacilityForm({resource, setResource}) {
- const formRef = useRef(0);
- const nameRef = useRef(null);
+ const formRef = useRef(null);
const sponsor = resource.contributors.find((r) => r.contributor_type === 'sponsor') || {};
const [name, setName] = useState(sponsor.contributor_name || '');
const [nameId, setNameId] = useState(sponsor.name_identifier_id);
@@ -65,10 +64,12 @@ export default function FacilityForm({resource, setResource}) {
htmlId: 'research_facility',
labelText: 'Research facility',
isRequired: false,
+ desBy: 'facility-ex',
}}
/>
-
-
+
+ A field or other station or organization where research was conducted, apart from affiliations
+
)}
diff --git a/app/javascript/react/components/MetadataEntry/Support/FunderForm.jsx b/app/javascript/react/components/MetadataEntry/Support/FunderForm.jsx
index 89984dc220..2d85b669a4 100644
--- a/app/javascript/react/components/MetadataEntry/Support/FunderForm.jsx
+++ b/app/javascript/react/components/MetadataEntry/Support/FunderForm.jsx
@@ -4,10 +4,10 @@ import {Field, Form, Formik} from 'formik';
import axios from 'axios';
import PropTypes from 'prop-types';
import RorAutocomplete from '../RorAutocomplete';
-import {showModalYNDialog, showSavedMsg, showSavingMsg} from '../../../../lib/utils';
+import {showSavedMsg, showSavingMsg} from '../../../../lib/utils';
function FunderForm({
- resourceId, contributor, removeFunction, updateFunder, groupings,
+ resourceId, contributor, updateFunder, groupings,
}) {
const formRef = useRef();
@@ -104,9 +104,11 @@ function FunderForm({
labelText: 'Granting organization',
isRequired: true,
errorId: 'funder_error',
+ desBy: `${contributor.id}funder-ex`,
}
}
/>
+
National Institutes of Health
{showSelect && (
<>
{showSelect.group_label}
@@ -125,10 +127,10 @@ function FunderForm({
name="award_number"
type="text"
className="js-award_number c-input__text"
- onBlur={() => { // defaults to formik.handleBlur
- formik.handleSubmit();
- }}
+ aria-describedby={`${contributor.id}award-ex`}
+ onBlur={formik.handleSubmit}
/>
+
CA 123456-01A1
Program/division
@@ -138,26 +140,11 @@ function FunderForm({
name="award_description"
type="text"
className="js-award_description c-input__text"
- onBlur={() => { // defaults to formik.handleBlur
- formik.handleSubmit();
- }}
+ aria-describedby={`${contributor.id}desc-ex`}
+ onBlur={formik.handleSubmit}
/>
+ Section of the funder
-
- {
- showModalYNDialog('Are you sure you want to remove this funder?', () => {
- removeFunction(contributor.id);
- });
- }}
- aria-label="Remove funding"
- title="Remove"
- >
-
-
-
)}
@@ -171,6 +158,5 @@ export default FunderForm;
FunderForm.propTypes = {
resourceId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
contributor: PropTypes.object.isRequired,
- removeFunction: PropTypes.func.isRequired,
updateFunder: PropTypes.func.isRequired,
};
diff --git a/app/javascript/react/components/MetadataEntry/Support/Funders.jsx b/app/javascript/react/components/MetadataEntry/Support/Funders.jsx
index b8071016a8..40dedad1c0 100644
--- a/app/javascript/react/components/MetadataEntry/Support/Funders.jsx
+++ b/app/javascript/react/components/MetadataEntry/Support/Funders.jsx
@@ -1,7 +1,7 @@
import React, {useState, useEffect} from 'react';
import axios from 'axios';
import PropTypes from 'prop-types';
-import {showSavedMsg, showSavingMsg} from '../../../../lib/utils';
+import {showSavedMsg, showSavingMsg, showModalYNDialog} from '../../../../lib/utils';
import DragonDropList, {DragonListItem, orderedItems} from '../DragonDropList';
import FunderForm from './FunderForm';
@@ -112,15 +112,20 @@ function Funders({resource, setResource}) {
{orderedItems({items: funders, typeName: 'funder'}).map((contrib) => (
-
+
+ {
+ showModalYNDialog('Are you sure you want to remove this funder?', () => {
+ removeItem(contrib.id);
+ });
+ }}
+ aria-label="Remove funding"
+ title="Remove"
+ >
+
+
))}
diff --git a/app/javascript/react/components/MetadataEntry/Support/Support.jsx b/app/javascript/react/components/MetadataEntry/Support/Support.jsx
index 726a1e6d84..37b1501e5c 100644
--- a/app/javascript/react/components/MetadataEntry/Support/Support.jsx
+++ b/app/javascript/react/components/MetadataEntry/Support/Support.jsx
@@ -7,7 +7,10 @@ export default function Support({resource, setResource}) {
<>
Support
-
Funding
+
+
Funding
+
Drag to reorder
+
>
);
diff --git a/app/javascript/react/components/ReadMeWizard/ReadMeSteps.jsx b/app/javascript/react/components/ReadMeWizard/ReadMeSteps.jsx
index de32f81521..6b21542ced 100644
--- a/app/javascript/react/components/ReadMeWizard/ReadMeSteps.jsx
+++ b/app/javascript/react/components/ReadMeWizard/ReadMeSteps.jsx
@@ -32,7 +32,7 @@ export default function ReadMeSteps({
2: {
desc:
<>
-
Provide a comprehensive list of data files and variables. Starting information has been imported from your uploaded files.
+
Provide a comprehensive list of data files and variables. Starting information has been imported from your uploaded files.