diff --git a/app/assets/scripts/components/documents/document-changelog-modal.js b/app/assets/scripts/components/documents/document-changelog-modal.js
index 3f76ca79..0d9de0d2 100644
--- a/app/assets/scripts/components/documents/document-changelog-modal.js
+++ b/app/assets/scripts/components/documents/document-changelog-modal.js
@@ -2,7 +2,7 @@ import React, { useEffect } from 'react';
import T from 'prop-types';
import styled from 'styled-components';
-import ReactGA from 'react-ga';
+import ReactGA from 'react-ga4';
import { Modal as BaseModal } from '@devseed-ui/modal';
import { glsp } from '@devseed-ui/theme-provider';
@@ -121,7 +121,7 @@ export default function DocumentChangelogModal(props) {
useEffect(() => {
if (revealed) {
- ReactGA.modalview('document-changelog');
+ ReactGA.send({ hitType: 'modalview', page: '/modal/document-changelog' });
}
}, [revealed]);
diff --git a/app/assets/scripts/components/documents/document-delete-process.js b/app/assets/scripts/components/documents/document-delete-process.js
index 7b54b78e..ab2ab2e1 100644
--- a/app/assets/scripts/components/documents/document-delete-process.js
+++ b/app/assets/scripts/components/documents/document-delete-process.js
@@ -1,5 +1,6 @@
import { confirmDeleteDocumentVersion } from '../common/confirmation-prompt';
import toasts from '../common/toasts';
+import ReactGA from 'react-ga4';
/**
* Convenience method to delete an atbd version and show a toast notification.
@@ -24,6 +25,7 @@ export async function documentDeleteVersionConfirmAndToast({
if (result.error) {
toasts.error(`An error occurred: ${result.error.message}`);
} else {
+ ReactGA.event('atbd_version_deleted');
toasts.success('Document version successfully deleted');
history.push('/dashboard');
}
diff --git a/app/assets/scripts/components/documents/document-download-menu.js b/app/assets/scripts/components/documents/document-download-menu.js
index 41f02eb7..79890857 100644
--- a/app/assets/scripts/components/documents/document-download-menu.js
+++ b/app/assets/scripts/components/documents/document-download-menu.js
@@ -1,6 +1,6 @@
import React, { useMemo } from 'react';
import T from 'prop-types';
-import ReactGA from 'react-ga';
+import ReactGA from 'react-ga4';
import { Auth } from 'aws-amplify';
import { saveAs } from 'file-saver';
diff --git a/app/assets/scripts/components/documents/document-info-modal.js b/app/assets/scripts/components/documents/document-info-modal.js
index 90c0e804..e5881bca 100644
--- a/app/assets/scripts/components/documents/document-info-modal.js
+++ b/app/assets/scripts/components/documents/document-info-modal.js
@@ -1,7 +1,7 @@
import React, { useCallback, useEffect } from 'react';
import T from 'prop-types';
import styled from 'styled-components';
-import ReactGA from 'react-ga';
+import ReactGA from 'react-ga4';
import { FormTextarea } from '@devseed-ui/form';
import { Modal } from '@devseed-ui/modal';
import { Button } from '@devseed-ui/button';
@@ -41,7 +41,7 @@ export default function DocumentInfoModal(props) {
useEffect(() => {
if (revealed) {
- ReactGA.modalview('document-info');
+ ReactGA.send({ hitType: 'modalview', page: '/modal/document-info' });
}
}, [revealed]);
diff --git a/app/assets/scripts/components/documents/document-tracker-modal.js b/app/assets/scripts/components/documents/document-tracker-modal.js
index 42a89198..1c99bebb 100644
--- a/app/assets/scripts/components/documents/document-tracker-modal.js
+++ b/app/assets/scripts/components/documents/document-tracker-modal.js
@@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useMemo } from 'react';
import T from 'prop-types';
import qs from 'qs';
-import ReactGA from 'react-ga';
+import ReactGA from 'react-ga4';
import styled, { css } from 'styled-components';
import { useLocation } from 'react-router';
import { Modal, ModalFooter } from '@devseed-ui/modal';
@@ -140,7 +140,10 @@ export default function DocumentTrackerModal(props) {
useEffect(() => {
if (revealed) {
- ReactGA.modalview('document-progress-tracker');
+ ReactGA.send({
+ hitType: 'modalview',
+ page: '/modal/document-progress-tracker'
+ });
}
}, [revealed]);
diff --git a/app/assets/scripts/components/documents/hub/document-hub-entry.js b/app/assets/scripts/components/documents/hub/document-hub-entry.js
index 8cba7044..ce0f9235 100644
--- a/app/assets/scripts/components/documents/hub/document-hub-entry.js
+++ b/app/assets/scripts/components/documents/hub/document-hub-entry.js
@@ -1,6 +1,6 @@
import React, { useMemo } from 'react';
import T from 'prop-types';
-import ReactGA from 'react-ga';
+import ReactGA from 'react-ga4';
import { VerticalDivider } from '@devseed-ui/toolbar';
import { BsFilePdf } from 'react-icons/bs';
diff --git a/app/assets/scripts/components/documents/journal-pdf-preview/index.js b/app/assets/scripts/components/documents/journal-pdf-preview/index.js
index f71e3103..48a27100 100644
--- a/app/assets/scripts/components/documents/journal-pdf-preview/index.js
+++ b/app/assets/scripts/components/documents/journal-pdf-preview/index.js
@@ -23,7 +23,10 @@ import {
formatReference,
sortReferences
} from '../../../utils/references';
-import { formatDocumentTableCaptions } from '../../../utils/format-table-captions';
+import { applyNumberCaptionsToDocument } from '../../../utils/apply-number-captions-to-document';
+import { VariableItem } from '../single-view/document-body';
+import { variableNodeType } from '../../../types';
+import { sortContacts } from '../../../utils/sort-contacts';
const ReferencesList = styled.ol`
&& {
@@ -217,6 +220,28 @@ ImplementationDataList.propTypes = {
)
};
+function VariablesList({ list }) {
+ if (!list || list.length === 0) {
+ return EMPTY_CONTENT_MESSAGE;
+ }
+
+ return (
+
+ {list?.map((variable, i) => (
+
+ ))}
+
+ );
+}
+
+VariablesList.propTypes = {
+ list: T.arrayOf(variableNodeType)
+};
+
function ContactOutput(props) {
const { data } = props;
const { affiliations, contact, roles } = data;
@@ -344,7 +369,7 @@ function JournalPdfPreview() {
journal_discussion,
data_availability,
journal_acknowledgements
- } = formatDocumentTableCaptions(document);
+ } = applyNumberCaptionsToDocument(document);
const ContentView = useMemo(() => {
const safeReadContext = {
@@ -422,43 +447,67 @@ function JournalPdfPreview() {
// create contacts list component with superscripts
let contacts = [];
- contacts_link?.forEach(
- ({ contact, affiliations: contactAffiliations }, i) => {
+ let authors = contacts_link?.filter(
+ (c) => !c.roles?.includes('Document Reviewer')
+ ); // Remove any reviewer from the authors list
+
+ authors
+ ?.sort(sortContacts)
+ .forEach(({ contact, affiliations: contactAffiliations }, i) => {
const hasAffiliation =
contactAffiliations && contactAffiliations.length > 0;
- let contactEmail = contact.mechanisms.find(
- (mechanism) => mechanism.mechanism_type === 'Email'
- )?.mechanism_value;
-
const item = (
{getContactName(contact, { full: true })}
- {contactEmail && ` (${contactEmail})`}
+ {hasAffiliation &&
+ contactAffiliations.map((affiliation, j) => {
+ return (
+ <>
+
+ {Array.from(affiliations).indexOf(affiliation) + 1}
+
+
+ {j < contactAffiliations.length - 1 && , }
+
+ >
+ );
+ })}
+ {i < authors.length - 1 && , }
+ {i === authors.length - 2 && and }
- {hasAffiliation &&
- contactAffiliations.map((affiliation, j) => {
- return (
- <>
-
- {Array.from(affiliations).indexOf(affiliation) + 1}
-
-
- {j < contactAffiliations.length - 1 && , }
-
- >
- );
- })}
- {i < contacts_link.length - 1 && , }
- {i === contacts_link.length - 2 && and }
);
contacts.push(item);
- }
- );
+ });
+
+ // create corresponding authors list component
+ const correspondingAuthors =
+ authors
+ ?.filter((c) =>
+ c.roles?.find((r) => r.toLowerCase() === 'corresponding author')
+ )
+ .map(({ contact }) => {
+ let contactEmail = contact.mechanisms.find(
+ (mechanism) => mechanism.mechanism_type === 'Email'
+ )?.mechanism_value;
+
+ let contactName = getContactName(contact, { full: true });
+
+ return `${contactName} ${contactEmail ? `(${contactEmail})` : ''}`;
+ }) || [];
+
+ const correspondingAuthorsString = correspondingAuthors.map((author, i) => (
+ <>
+ {author}
+ {i < correspondingAuthors.length - 1 && , }
+ {i === correspondingAuthors.length - 2 && and }
+ >
+ ));
return {
items: contacts,
+ correspondingAuthors: correspondingAuthorsString,
affiliations_list: Array.from(affiliations),
maxIndex: (contacts_link?.length ?? 0) - 1
};
@@ -514,10 +563,9 @@ function JournalPdfPreview() {
);
const mathematicalTheoryVisible =
hasContent(mathematical_theory) || mathematicalTheoryAssumptionsVisible;
- const algorithmInputVariablesVisible = hasContent(algorithm_input_variables);
- const algorithmOutputVariablesVisible = hasContent(
- algorithm_output_variables
- );
+ const algorithmInputVariablesVisible = algorithm_input_variables?.length > 0;
+ const algorithmOutputVariablesVisible =
+ algorithm_output_variables?.length > 0;
const algorithmDescriptionVisible =
scientificTheoryVisible ||
mathematicalTheoryVisible ||
@@ -581,6 +629,14 @@ function JournalPdfPreview() {
))}
+
+ {contacts?.correspondingAuthors?.length > 0 && (
+
+ Corresponding Author(s):
+ {contacts?.correspondingAuthors}
+
+ )}
+
@@ -668,7 +724,7 @@ function JournalPdfPreview() {
id='algorithm_input_variables'
title='Algorithm Input Variables'
>
-
+
)}
{algorithmOutputVariablesVisible && (
@@ -676,7 +732,7 @@ function JournalPdfPreview() {
id='algorithm_output_variables'
title='Algorithm Output Variables'
>
-
+
)}
diff --git a/app/assets/scripts/components/documents/pdf-preview/index.js b/app/assets/scripts/components/documents/pdf-preview/index.js
index 89661041..8c621910 100644
--- a/app/assets/scripts/components/documents/pdf-preview/index.js
+++ b/app/assets/scripts/components/documents/pdf-preview/index.js
@@ -9,6 +9,7 @@ import DocumentBody from '../single-view/document-body';
import DocumentTitle from '../single-view/document-title';
import { DocumentProse } from '../single-view/document-content';
import { ScrollAnchorProvider } from '../single-view/scroll-manager';
+import { applyNumberCaptionsToDocument } from '../../../utils/apply-number-captions-to-document';
const TocHeader = styled.h1`
border-bottom: 3px solid #000;
@@ -204,13 +205,6 @@ function generateTocAndHeadingNumbering(content) {
// Starting from h2
generateHeading(2, tocElement, content, undefined);
- const captions = content.querySelectorAll('.slate-image-block figcaption');
- Array.from(captions).forEach((caption, i) => {
- const captionPrefix = document.createElement('span');
- captionPrefix.innerText = `Figure ${i + 1}: `;
- caption.prepend(captionPrefix);
- });
-
const equationNumbers = content.querySelectorAll(
'.slate-equation-element .equation-number'
);
@@ -225,6 +219,7 @@ function PdfPreview() {
const { isAuthReady } = useUser();
const contentRef = useRef(null);
const [previewReady, setPreviewReady] = useState(false);
+ const [document, setDocument] = useState(null);
useEffect(() => {
isAuthReady && fetchSingleAtbd();
@@ -245,12 +240,19 @@ function PdfPreview() {
}
if (atbd.status === 'succeeded') {
- generateTocAndHeadingNumbering(contentRef.current);
-
+ setDocument(applyNumberCaptionsToDocument(atbd.data.document));
waitForImages();
}
}, [atbd.status]);
+ // This useEffect is responsible for generating the ToC and numbering
+ // after the document is transformed
+ useEffect(() => {
+ if (document && contentRef?.current) {
+ generateTocAndHeadingNumbering(contentRef.current);
+ }
+ }, [document]);
+
return (
{atbd.status === 'loading' && }
@@ -271,9 +273,17 @@ function PdfPreview() {
id='table-of-contents'
/>
-
-
-
+ {document && (
+
+
+
+ )}
{previewReady && }
diff --git a/app/assets/scripts/components/documents/single-edit/step-contacts/contact-fieldset.js b/app/assets/scripts/components/documents/single-edit/step-contacts/contact-fieldset.js
index 1af683cb..9cffb06e 100644
--- a/app/assets/scripts/components/documents/single-edit/step-contacts/contact-fieldset.js
+++ b/app/assets/scripts/components/documents/single-edit/step-contacts/contact-fieldset.js
@@ -38,7 +38,8 @@ const roleTypes = [
'Supervision',
'Investigation',
'Funding acquisition',
- 'Corresponding Author'
+ 'Corresponding Author',
+ 'Document Reviewer'
];
const emptyAffiliation = '';
diff --git a/app/assets/scripts/components/documents/single-edit/step-contacts/index.js b/app/assets/scripts/components/documents/single-edit/step-contacts/index.js
index 99498875..985b0097 100644
--- a/app/assets/scripts/components/documents/single-edit/step-contacts/index.js
+++ b/app/assets/scripts/components/documents/single-edit/step-contacts/index.js
@@ -1,12 +1,9 @@
import React, { useCallback, useEffect } from 'react';
import T from 'prop-types';
import set from 'lodash.set';
-import get from 'lodash.get';
-import { FieldArray, Formik, Form as FormikForm } from 'formik';
+import { Formik, Form as FormikForm } from 'formik';
import { Form } from '@devseed-ui/form';
import { GlobalLoading } from '@devseed-ui/global-loading';
-import { glsp } from '@devseed-ui/theme-provider';
-import styled from 'styled-components';
import { Inpage, InpageBody } from '../../../../styles/inpage';
import {
@@ -14,10 +11,7 @@ import {
FormBlockHeading,
FormSectionNotes
} from '../../../../styles/form-block';
-import {
- FormikSectionFieldset,
- SectionFieldset
-} from '../../../common/forms/section-fieldset';
+import { FormikSectionFieldset } from '../../../common/forms/section-fieldset';
import ContactsList from './contacts-list';
import { Link } from '../../../../styles/clean/link';
@@ -29,17 +23,6 @@ import { getDocumentSectionLabel } from '../sections';
import { documentEdit } from '../../../../utils/url-creator';
import { LocalStore } from '../local-store';
import { FormikUnloadPrompt } from '../../../common/unload-prompt';
-import { FormikInputText } from '../../../common/forms/input-text';
-import { DeletableFieldset } from '../../../common/forms/deletable-fieldset';
-import { FieldMultiItem } from '../../../common/forms/field-multi-item';
-
-const emptyAffiliation = '';
-
-const BasicInfoSection = styled.div`
- display: grid;
- grid-gap: ${glsp()};
- grid-template-columns: 1fr 1fr;
-`;
export default function StepContacts(props) {
const { renderInpageHeader, renderFormFooter, atbd, id, version, step } =
@@ -148,58 +131,6 @@ export default function StepContacts(props) {
-
-
-
-
-
-
- {
- const fieldValues = get(form.values, affFieldName) || [];
- return (
- push(emptyAffiliation)}
- >
- {fieldValues.map((field, index) => (
- remove(index)}
- >
-
-
- ))}
-
- );
- }}
- />
-
{renderFormFooter()}
diff --git a/app/assets/scripts/components/documents/single-edit/steps.js b/app/assets/scripts/components/documents/single-edit/steps.js
index b2ed1875..8939d97d 100644
--- a/app/assets/scripts/components/documents/single-edit/steps.js
+++ b/app/assets/scripts/components/documents/single-edit/steps.js
@@ -58,7 +58,6 @@ const STEP_CONTACTS = {
// affiliations: []
// }
],
- reviewer_info: atbd?.reviewer_info,
sections_completed: {
contacts: 'incomplete'
}
diff --git a/app/assets/scripts/components/documents/single-edit/use-document-create.js b/app/assets/scripts/components/documents/single-edit/use-document-create.js
index 75378407..437f656c 100644
--- a/app/assets/scripts/components/documents/single-edit/use-document-create.js
+++ b/app/assets/scripts/components/documents/single-edit/use-document-create.js
@@ -1,5 +1,6 @@
import { useCallback } from 'react';
import { useHistory } from 'react-router';
+import ReactGA from 'react-ga4';
import { useAtbds } from '../../../context/atbds-list';
import { documentEdit } from '../../../utils/url-creator';
@@ -33,6 +34,10 @@ export function useDocumentCreate(title, alias, isPdfType) {
processToast.error(`An error occurred: ${result.error.message}`);
}
} else {
+ ReactGA.event('atbd_created', {
+ type: isPdfType ? 'pdf' : 'regular'
+ });
+
processToast.success('Document successfully created');
// To trigger the modals to open from other pages, we use the history
// state as the user is sent from one page to another. See explanation
diff --git a/app/assets/scripts/components/documents/single-edit/use-submit.js b/app/assets/scripts/components/documents/single-edit/use-submit.js
index ccc3ed36..757de42d 100644
--- a/app/assets/scripts/components/documents/single-edit/use-submit.js
+++ b/app/assets/scripts/components/documents/single-edit/use-submit.js
@@ -1,5 +1,6 @@
import { useCallback } from 'react';
import { useHistory } from 'react-router';
+import ReactGA from 'react-ga4';
import { documentEdit } from '../../../utils/url-creator';
import { createProcessToast } from '../../common/toasts';
@@ -22,6 +23,7 @@ export function useSubmitForMetaAndVersionData(updateAtbd, atbd, step) {
if (result.error) {
processToast.error(`An error occurred: ${result.error.message}`);
} else {
+ ReactGA.event('atbd_updated');
resetForm({ values });
processToast.success('Changes saved');
// Update the path in case the alias changed.
@@ -81,6 +83,7 @@ export function useSubmitForVersionData(updateAtbd, atbd, hook) {
if (result.error) {
processToast.error(`An error occurred: ${result.error.message}`);
} else {
+ ReactGA.event('atbd_updated');
formBag.resetForm({ values });
processToast.success('Changes saved');
@@ -300,6 +303,7 @@ export function useSubmitForAtbdContacts({
`An error occurred: ${result.error.message}. Please try again`
);
} else {
+ ReactGA.event('atbd_updated');
resetForm({
values: {
...values,
diff --git a/app/assets/scripts/components/documents/single-view/document-body.js b/app/assets/scripts/components/documents/single-view/document-body.js
index a3a6ca3d..855f8e41 100644
--- a/app/assets/scripts/components/documents/single-view/document-body.js
+++ b/app/assets/scripts/components/documents/single-view/document-body.js
@@ -27,8 +27,7 @@ import { useCommentCenter } from '../../../context/comment-center';
import { isJournalPublicationIntended } from '../status';
import serializeSlateToString from '../../slate/serialize-to-string';
import { useContextualAbility } from '../../../a11n';
-import { isDefined, isTruthyString } from '../../../utils/common';
-import { formatDocumentTableCaptions } from '../../../utils/format-table-captions';
+import { sortContacts } from '../../../utils/sort-contacts';
const PDFPreview = styled.iframe`
width: 100%;
@@ -216,7 +215,7 @@ const DataAccessItem = ({ id, label, url, description }) => (
);
-const VariableItem = ({ element, variable }) => (
+export const VariableItem = ({ element, variable }) => (
{element.label}
@@ -301,7 +300,7 @@ const ContactItem = ({ id, label, contact, roles, affiliations }) => (
);
-const EmptySection = ({ className }) => (
+export const EmptySection = ({ className }) => (
No content available.
);
@@ -1051,58 +1050,49 @@ const htmlAtbdContentSections = [
),
children: ({ atbd }) => {
const contactsLink = atbd?.contacts_link || [];
- return contactsLink.map(({ contact, roles, affiliations }, idx) => ({
- label: getContactName(contact),
- id: `contacts_${idx + 1}`,
- render: ({ element }) => (
-
+ return contactsLink
+ .filter(
+ ({ roles }) =>
+ // Remove reviewers that have the role 'Document Reviewer'
+ !roles.includes('Document Reviewer')
)
- }));
+ .sort(sortContacts)
+ .map(({ contact, roles, affiliations }, idx) => ({
+ label: getContactName(contact),
+ id: `contacts_${idx + 1}`,
+ render: ({ element }) => (
+
+ )
+ }));
}
},
{
label: 'Reviewer Information',
id: 'reviewer_info',
shouldRender: ({ atbd }) => {
- if (!atbd || !atbd.reviewer_info) {
- return false;
- }
+ if (!atbd) return false;
- const {
- reviewer_info: { first_name, last_name }
- } = atbd;
+ // Render if there are reviewers with the role 'Document Reviewer'
+ const contactsLink = atbd?.contacts_link || [];
- if (!isTruthyString(first_name) && !isTruthyString(last_name)) {
- return false;
+ for (const { roles } of contactsLink) {
+ if (roles.includes('Document Reviewer')) {
+ return true;
+ }
}
-
- return true;
+ return false;
},
- render: ({ element, atbd, printMode }) => {
- if (!atbd || !atbd.reviewer_info) {
- return null;
- }
-
- const {
- reviewer_info: { first_name, last_name, email, affiliations }
- } = atbd;
-
- let fullName;
- if (isTruthyString(first_name) || isTruthyString(last_name)) {
- fullName = [first_name, last_name].filter(isDefined).join(' ');
- }
-
- if (!isTruthyString(fullName) && !isTruthyString(email)) {
+ render: ({ element, atbd, printMode, children }) => {
+ if (!atbd) {
return null;
}
-
return (
-
- {fullName}
-
- Email
- {email}
- Affiliations
- {affiliations.length ? (
- {renderMultipleStringValues(affiliations)}
- ) : (
- No affiliations for the reviewer
- )}
-
-
+ {React.Children.count(children) ? (
+ children
+ ) : (
+ There are no reviewers associated with this document
+ )}
);
+ },
+ children: ({ atbd }) => {
+ const contactsLink = atbd?.contacts_link || [];
+ return contactsLink
+ .filter(({ roles }) =>
+ // Include reviewers that have the role 'Document Reviewer'
+ roles.includes('Document Reviewer')
+ )
+ .sort(sortContacts)
+ .map(({ contact, roles, affiliations }, idx) => ({
+ label: getContactName(contact),
+ id: `contacts_${idx + 1}`,
+ render: ({ element }) => (
+
+ )
+ }));
}
},
{
@@ -1268,58 +1272,48 @@ const pdfAtbdContentSections = [
),
children: ({ atbd }) => {
const contactsLink = atbd?.contacts_link || [];
- return contactsLink.map(({ contact, roles, affiliations }, idx) => ({
- label: getContactName(contact),
- id: `contacts_${idx + 1}`,
- render: ({ element }) => (
-
+ return contactsLink
+ .filter(
+ ({ roles }) =>
+ // Remove reviewers that have the role 'Document Reviewer'
+ !roles.includes('Document Reviewer')
)
- }));
+ .map(({ contact, roles, affiliations }, idx) => ({
+ label: getContactName(contact),
+ id: `contacts_${idx + 1}`,
+ render: ({ element }) => (
+
+ )
+ }));
}
},
{
label: 'Reviewer Information',
id: 'reviewer_info',
shouldRender: ({ atbd }) => {
- if (!atbd || !atbd.reviewer_info) {
- return false;
- }
+ if (!atbd) return false;
- const {
- reviewer_info: { first_name, last_name }
- } = atbd;
+ // Render if there are reviewers with the role 'Document Reviewer'
+ const contactsLink = atbd?.contacts_link || [];
- if (!isTruthyString(first_name) && !isTruthyString(last_name)) {
- return false;
+ for (const { roles } of contactsLink) {
+ if (roles.includes('Document Reviewer')) {
+ return true;
+ }
}
-
- return true;
+ return false;
},
- render: ({ element, atbd, printMode }) => {
- if (!atbd || !atbd.reviewer_info) {
+ render: ({ element, atbd, printMode, children }) => {
+ if (!atbd) {
return null;
}
-
- const {
- reviewer_info: { first_name, last_name, email, affiliations }
- } = atbd;
-
- let fullName;
- if (isTruthyString(first_name) || isTruthyString(last_name)) {
- fullName = [first_name, last_name].filter(isDefined).join(' ');
- }
-
- if (!isTruthyString(fullName) && !isTruthyString(email)) {
- return null;
- }
-
return (
-
- {fullName}
-
- Email
- {email}
- Affiliations
- {affiliations.length ? (
- {renderMultipleStringValues(affiliations)}
- ) : (
- No affiliations for the reviewer
- )}
-
-
+ {React.Children.count(children) ? (
+ children
+ ) : (
+ There are no reviewers associated with this document
+ )}
);
}
@@ -1456,7 +1441,7 @@ export default function DocumentBody(props) {
);
return renderElements(getAtbdContentSections(atbd.document_type === 'PDF'), {
- document: formatDocumentTableCaptions(document),
+ document,
referencesUseIndex,
referenceList,
atbd,
diff --git a/app/assets/scripts/components/home/index.js b/app/assets/scripts/components/home/index.js
index cff7b2e7..7eb2bc17 100644
--- a/app/assets/scripts/components/home/index.js
+++ b/app/assets/scripts/components/home/index.js
@@ -1,6 +1,6 @@
import React from 'react';
import styled from 'styled-components';
-import ReactGA from 'react-ga';
+import ReactGA from 'react-ga4';
import { glsp, rgba, themeVal } from '@devseed-ui/theme-provider';
import { Heading } from '@devseed-ui/typography';
import { Button } from '@devseed-ui/button';
diff --git a/app/assets/scripts/components/new-atbd/index.js b/app/assets/scripts/components/new-atbd/index.js
index 513c4cbd..12a801ac 100644
--- a/app/assets/scripts/components/new-atbd/index.js
+++ b/app/assets/scripts/components/new-atbd/index.js
@@ -2,6 +2,7 @@ import React from 'react';
import styled from 'styled-components';
import { themeVal, glsp } from '@devseed-ui/theme-provider';
import { Button } from '@devseed-ui/button';
+import ReactGA from 'react-ga4';
import {
FaFileAlt,
@@ -232,6 +233,11 @@ function NewAtbd() {
href='https://docs.google.com/document/d/1T4q56qZrRN5L6MGXA1UJLMgDgS-Fde9Fo4R4bwVQDF8/edit?usp=sharing'
target='_blank'
rel='noopener noreferrer'
+ onClick={() => {
+ ReactGA.event('atbd_template_download', {
+ template_format: 'Google Docs'
+ });
+ }}
>
@@ -242,6 +248,11 @@ function NewAtbd() {
href='https://docs.google.com/document/d/1Jh3htOiivNIG_ZqhbN5nEK1TAVB6BjRY/edit?usp=share_link&ouid=102031143611308171378&rtpof=true&sd=true'
target='_blank'
rel='noopener noreferrer'
+ onClick={() => {
+ ReactGA.event('atbd_template_download', {
+ template_format: 'Microsoft Word'
+ });
+ }}
>
@@ -251,7 +262,12 @@ function NewAtbd() {
{
+ ReactGA.event('atbd_template_download', {
+ template_format: 'Latex'
+ });
+ }}
>
diff --git a/app/assets/scripts/components/slate/plugins/caption/caption.js b/app/assets/scripts/components/slate/plugins/caption/caption.js
index 1efad477..c8d292ad 100644
--- a/app/assets/scripts/components/slate/plugins/caption/caption.js
+++ b/app/assets/scripts/components/slate/plugins/caption/caption.js
@@ -4,9 +4,6 @@ import { Node } from 'slate';
import { useReadOnly, useSelected } from 'slate-react';
import styled from 'styled-components';
-import { NumberingContext } from '../../../../context/numbering';
-import { IMAGE_BLOCK, TABLE_BLOCK } from '../constants';
-
const CaptionElement = styled.figcaption`
font-size: 0.875rem;
line-height: 1.25rem;
@@ -27,42 +24,11 @@ export function Caption(props) {
const { attributes, htmlAttributes, element, children } = props;
const isSelected = useSelected();
const readOnly = useReadOnly();
- const id = JSON.stringify(element);
- const { parent } = element;
const emptyCaption = !Node.string(element);
- const numberingContext = React.useContext(NumberingContext);
const showPlaceholder = !readOnly && !isSelected && emptyCaption;
- React.useEffect(() => {
- if (numberingContext && !showPlaceholder && id) {
- if (parent === TABLE_BLOCK) {
- numberingContext.registerTable(id);
- } else if (parent === IMAGE_BLOCK) {
- numberingContext.registerImage(id);
- }
- }
- }, [numberingContext, showPlaceholder, id, parent]);
-
- const numbering = React.useMemo(() => {
- if (!numberingContext) {
- return null;
- }
-
- if (parent === TABLE_BLOCK) {
- return numberingContext.getTableNumbering(id);
- }
-
- if (parent === IMAGE_BLOCK) {
- return numberingContext.getImageNumbering(id);
- }
-
- return null;
- }, [numberingContext, parent, id]);
-
- // if (readOnly && emptyCaption) return null;
-
// The current version of Slate has no way to render a placeholder on an
// element. The best way is to create an element which is absolutely
// positioned and has no interaction.
@@ -71,7 +37,6 @@ export function Caption(props) {
{showPlaceholder && (
Write a caption
)}
- {numbering}
{children}
);
diff --git a/app/assets/scripts/components/slate/plugins/equation/equation-element.js b/app/assets/scripts/components/slate/plugins/equation/equation-element.js
index f3f4d91b..3ed667bc 100644
--- a/app/assets/scripts/components/slate/plugins/equation/equation-element.js
+++ b/app/assets/scripts/components/slate/plugins/equation/equation-element.js
@@ -42,6 +42,7 @@ function EquationElement(props) {
const isSelected = useSelected();
const { element, attributes, children } = props;
const latexEquation = Node.string(element);
+ const equationPath = JSON.stringify(ReactEditor.findPath(editor, element));
const readOnly = useReadOnly();
const handleClick = useCallback(() => {
@@ -60,9 +61,9 @@ function EquationElement(props) {
useEffect(() => {
if (numberingContext && !isInline) {
- numberingContext.registerEquation(latexEquation);
+ numberingContext.registerEquation(equationPath);
}
- }, [numberingContext, isInline, latexEquation]);
+ }, [numberingContext, isInline, equationPath]);
const returnElement = React.useMemo(() => {
if (readOnly) {
@@ -83,7 +84,7 @@ function EquationElement(props) {
{!isInline && numberingContext && (
- {numberingContext.getEquationNumbering(latexEquation)}
+ {numberingContext.getEquationNumbering(equationPath)}
)}
{!isInline && }
@@ -110,6 +111,7 @@ function EquationElement(props) {
isInline,
isSelected,
latexEquation,
+ equationPath,
numberingContext,
readOnly
]);
diff --git a/app/assets/scripts/config/production.js b/app/assets/scripts/config/production.js
index 3f08b97d..11f6c7fc 100644
--- a/app/assets/scripts/config/production.js
+++ b/app/assets/scripts/config/production.js
@@ -14,6 +14,7 @@ module.exports = {
// Manually set the authentication flow type. Default is 'USER_SRP_AUTH'
authenticationFlowType: 'USER_PASSWORD_AUTH'
},
+ gaTrackingCode: 'UA-163103126-2',
hostedAuthUi:
'https://nasa-apt-api-lambda-prod-v2.auth.us-west-2.amazoncognito.com',
feedbackForm: 'https://forms.gle/JG6ykqj2mAjzke6S6'
diff --git a/app/assets/scripts/config/staging.js b/app/assets/scripts/config/staging.js
index 20f85ac0..be054953 100644
--- a/app/assets/scripts/config/staging.js
+++ b/app/assets/scripts/config/staging.js
@@ -1,6 +1,5 @@
// module exports is required to be able to load from gulpfile.
module.exports = {
- gaTrackingCode: 'UA-163103126-1',
apiUrl: 'https://af7f32q2kh.execute-api.us-east-1.amazonaws.com/v2',
auth: {
// DOCS: https://docs.amplify.aws/lib/auth/start/q/platform/js#re-use-existing-authentication-resource
@@ -13,5 +12,6 @@ module.exports = {
// Manually set the authentication flow type. Default is 'USER_SRP_AUTH'
authenticationFlowType: 'USER_PASSWORD_AUTH'
},
+ gaTrackingCode: 'G-K4DQR9HTZH',
hostedAuthUi: 'https://nasa-apt-api-staging.auth.us-east-1.amazoncognito.com'
};
diff --git a/app/assets/scripts/context/numbering.js b/app/assets/scripts/context/numbering.js
index eff391e8..1c59c851 100644
--- a/app/assets/scripts/context/numbering.js
+++ b/app/assets/scripts/context/numbering.js
@@ -1,9 +1,10 @@
import { createContext, useCallback, useMemo, useState } from 'react';
+/**
+ * Provides a context for equation numbering.
+ */
export function useNumberingProviderValue() {
const [registeredEquations, setRegisteredEquations] = useState({});
- const [registeredImages, setRegisteredImages] = useState({});
- const [registeredTables, setRegisteredTables] = useState({});
const registerEquation = useCallback((key) => {
setRegisteredEquations((prevElements) => {
@@ -19,34 +20,6 @@ export function useNumberingProviderValue() {
});
}, []);
- const registerImage = useCallback((key) => {
- setRegisteredImages((prevElements) => {
- if (prevElements[key]) {
- return prevElements;
- }
-
- const numElements = Object.keys(prevElements).length;
- return {
- ...prevElements,
- [key]: numElements + 1
- };
- });
- }, []);
-
- const registerTable = useCallback((key) => {
- setRegisteredTables((prevElements) => {
- if (prevElements[key]) {
- return prevElements;
- }
-
- const numElements = Object.keys(prevElements).length;
- return {
- ...prevElements,
- [key]: numElements + 1
- };
- });
- }, []);
-
const getEquationNumbering = useCallback(
(key) => {
const numbering = registeredEquations[key];
@@ -59,47 +32,12 @@ export function useNumberingProviderValue() {
[registeredEquations]
);
- const getTableNumbering = useCallback(
- (key) => {
- const numbering = registeredTables[key];
- if (!numbering) {
- return '';
- }
-
- return `Table ${numbering}: `;
- },
- [registeredTables]
- );
-
- const getImageNumbering = useCallback(
- (key) => {
- const numbering = registeredImages[key];
- if (!numbering) {
- return '';
- }
-
- return `Figure ${numbering}: `;
- },
- [registeredImages]
- );
-
return useMemo(
() => ({
getEquationNumbering,
- getTableNumbering,
- getImageNumbering,
- registerEquation,
- registerTable,
- registerImage
+ registerEquation
}),
- [
- getEquationNumbering,
- getTableNumbering,
- getImageNumbering,
- registerEquation,
- registerTable,
- registerImage
- ]
+ [getEquationNumbering, registerEquation]
);
}
diff --git a/app/assets/scripts/main.js b/app/assets/scripts/main.js
index 791d811c..94541eff 100644
--- a/app/assets/scripts/main.js
+++ b/app/assets/scripts/main.js
@@ -13,7 +13,7 @@ import {
useLocation,
useHistory
} from 'react-router-dom';
-import ReactGA from 'react-ga';
+import ReactGA from 'react-ga4';
import qs from 'qs';
import { DevseedUiThemeProvider } from '@devseed-ui/theme-provider';
import { CollecticonsGlobalStyle } from '@devseed-ui/collecticons';
@@ -84,9 +84,15 @@ const { gaTrackingCode } = config;
// Google analytics
if (gaTrackingCode) {
ReactGA.initialize(gaTrackingCode);
- ReactGA.pageview(window.location.pathname + window.location.search);
+ ReactGA.send({
+ hitType: 'pageview',
+ page: window.location.pathname + window.location.search
+ });
history.listen((location) =>
- ReactGA.pageview(location.pathname + location.search)
+ ReactGA.send({
+ hitType: 'pageview',
+ page: location.pathname + location.search
+ })
);
}
diff --git a/app/assets/scripts/types.js b/app/assets/scripts/types.js
new file mode 100644
index 00000000..852bc20e
--- /dev/null
+++ b/app/assets/scripts/types.js
@@ -0,0 +1,24 @@
+import PropTypes from 'prop-types';
+
+// Basic text child type
+const textChildType = PropTypes.shape({
+ text: PropTypes.string.isRequired
+});
+
+// Type for a paragraph element with children
+const pChildType = PropTypes.shape({
+ type: PropTypes.string.isRequired,
+ children: PropTypes.arrayOf(textChildType).isRequired
+});
+
+// Type for a variable node with children
+const variableNodePropType = PropTypes.shape({
+ children: PropTypes.arrayOf(pChildType).isRequired
+});
+
+// Type for a variable item in the list
+export const variableNodeType = PropTypes.shape({
+ name: variableNodePropType.isRequired,
+ long_name: variableNodePropType.isRequired,
+ unit: variableNodePropType.isRequired
+});
diff --git a/app/assets/scripts/utils/format-table-captions.js b/app/assets/scripts/utils/apply-number-captions-to-document.js
similarity index 58%
rename from app/assets/scripts/utils/format-table-captions.js
rename to app/assets/scripts/utils/apply-number-captions-to-document.js
index 609c1579..26cd4eec 100644
--- a/app/assets/scripts/utils/format-table-captions.js
+++ b/app/assets/scripts/utils/apply-number-captions-to-document.js
@@ -1,10 +1,16 @@
import get from 'lodash.get';
-import { TABLE_BLOCK } from '../components/slate/plugins/constants';
+import {
+ IMAGE_BLOCK,
+ TABLE_BLOCK
+} from '../components/slate/plugins/constants';
/**
- * Include table numbers and move captions before the table in the document.
+ * Include tables and figures numbers to their captions. We don't use a
+ * numbering context (like equation numbering) because we also need to change
+ * the position of the caption in the document, which is not possible with the
+ * current implementation of equation numbering context.
*/
-export function formatDocumentTableCaptions(document) {
+export function applyNumberCaptionsToDocument(document) {
// Section id list in the order they should appear in the document
const documentSectionIds = [
'key_points',
@@ -34,6 +40,11 @@ export function formatDocumentTableCaptions(document) {
'journal_acknowledgements'
];
+ let elementCount = {
+ [TABLE_BLOCK]: 0,
+ [IMAGE_BLOCK]: 0
+ };
+
// Process sections to add table numbers to captions
return documentSectionIds.reduce(
(doc, sectionId) => {
@@ -50,51 +61,58 @@ export function formatDocumentTableCaptions(document) {
};
}
- // Init table count for this section
- let tableCount = doc.tableCount;
-
const nextDoc = {
...doc,
[sectionId]: {
...section,
children: section.children.map((child) => {
- // Ignore non-table blocks
- if (child.type !== TABLE_BLOCK) {
- return child;
- }
+ // Transform the table and image blocks
+ if (child.type === TABLE_BLOCK || child.type === IMAGE_BLOCK) {
+ elementCount[child.type] += 1;
+
+ const captionPrefix = `${
+ child.type === TABLE_BLOCK ? 'Table' : 'Figure'
+ } ${elementCount[child.type]}: `;
- // Reverse the table rows to make caption appear first
- // and add the table number to the caption
- return {
- ...child,
- children: child.children.reverse().map((c) => {
+ // Prefix the caption with the table/image number
+ const children = child.children.map((c) => {
if (c.type !== 'caption') {
return c;
}
const currentCaption = get(c, 'children[0].text');
- tableCount++;
return {
...c,
children: [
{
...c.children[0],
- text: `Table ${tableCount}: ${currentCaption}`
+ text: `${captionPrefix}${currentCaption}`
}
]
};
- })
- };
+ });
+
+ // Table should be reversed to make the caption appear first
+ if (child.type === TABLE_BLOCK) {
+ children.reverse();
+ }
+
+ return {
+ ...child,
+ children
+ };
+ }
+
+ return child;
})
}
};
return {
- ...nextDoc,
- tableCount: tableCount
+ ...nextDoc
};
},
- { ...document, tableCount: 0 }
+ { ...document }
);
}
diff --git a/app/assets/scripts/utils/sort-contacts.js b/app/assets/scripts/utils/sort-contacts.js
new file mode 100644
index 00000000..348f5565
--- /dev/null
+++ b/app/assets/scripts/utils/sort-contacts.js
@@ -0,0 +1,20 @@
+/**
+ * Sorts an array of contacts based on whether they have the "Corresponding Author" role.
+ * Corresponding authors appear first followed by other contacts
+ * @param {Object} a - The first contact object to compare.
+ * @param {Object} b - The second contact object to compare.
+ * @returns {number} - Returns -1 if a has the "Corresponding Author" role and b does not, 1 if b has the "Corresponding Author" role and a does not, and 0 if both have or do not have the "Corresponding Author" role.
+ */
+export function sortContacts(a, b) {
+ // Sort so that corresponding author roles are first
+ const hasCorrespondingAuthorRole = (roles) => {
+ return roles.includes('Corresponding Author');
+ };
+ const aHasCorrespondingAuthorRole = hasCorrespondingAuthorRole(a.roles);
+ const bHasCorrespondingAuthorRole = hasCorrespondingAuthorRole(b.roles);
+ return aHasCorrespondingAuthorRole === bHasCorrespondingAuthorRole
+ ? 0
+ : aHasCorrespondingAuthorRole
+ ? -1
+ : 1;
+}
diff --git a/package.json b/package.json
index efdae82f..79b7e106 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "apt-frontend",
"description": "Interface for the Algorithm Publication Tool",
- "version": "2.4.1-beta",
+ "version": "2.5.0",
"author": {
"name": "Development Seed",
"url": "https://developmentseed.org"
@@ -129,6 +129,7 @@
"react-cool-dimensions": "^1.3.2",
"react-dom": "^17.0.1",
"react-ga": "^3.3.0",
+ "react-ga4": "^2.1.0",
"react-helmet": "^6.1.0",
"react-icons": "^4.8.0",
"react-katex": "^2.0.2",
diff --git a/yarn.lock b/yarn.lock
index 58581832..c65d542f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -10038,6 +10038,11 @@ react-fast-compare@^3.1.1:
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz#929a97a532304ce9fee4bcae44234f1ce2c21d49"
integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==
+react-ga4@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/react-ga4/-/react-ga4-2.1.0.tgz#56601f59d95c08466ebd6edfbf8dede55c4678f9"
+ integrity sha512-ZKS7PGNFqqMd3PJ6+C2Jtz/o1iU9ggiy8Y8nUeksgVuvNISbmrQtJiZNvC/TjDsqD0QlU5Wkgs7i+w9+OjHhhQ==
+
react-ga@^3.3.0:
version "3.3.1"
resolved "https://registry.yarnpkg.com/react-ga/-/react-ga-3.3.1.tgz#d8e1f4e05ec55ed6ff944dcb14b99011dfaf9504"