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} +
+ )} +
)} {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"