diff --git a/PhotoUploadDevNotes.md b/PhotoUploadDevNotes.md deleted file mode 100644 index ff99393928..0000000000 --- a/PhotoUploadDevNotes.md +++ /dev/null @@ -1,13 +0,0 @@ -# Photo Upload - -There are currently three places to change the default photo upload size and default max count amount. These three places affect these pages or modals: `Submit -> Report an Encounter`, `Submit -> Report an Encounter (classic)`, `Submit -> Bulk Import`, `Submit -> Bulk Import -> See Instructions`. - -This is linked from comments in the relevant files which say: `Refer to PhotoUploadDevNotes.md for information/where else to change`. Non-development properties files are not annotated with this comment since those files will be changed by users. - -Order of using: -1. `commonConfiguration.properties`, which each production Wildbook can change if needed - - ./devops/deploy/.dockerfiles/tomcat/commonConfiguration.properties - - ./devops/development/.dockerfiles/tomcat/commonConfiguration.properties - - ./src/main/resources/bundles/commonConfiguration.properties -2. `org/ecocean/CommonConfiguration.java`, first fallback if properties are not initialized -3. `frontend/src/constants/photoUpload.js`, second fallback if SiteSettings is not accessible \ No newline at end of file diff --git a/devops/deploy/.dockerfiles/tomcat/commonConfiguration.properties b/devops/deploy/.dockerfiles/tomcat/commonConfiguration.properties index d44f8b4888..b11359fe7a 100644 --- a/devops/deploy/.dockerfiles/tomcat/commonConfiguration.properties +++ b/devops/deploy/.dockerfiles/tomcat/commonConfiguration.properties @@ -329,9 +329,7 @@ biologicalMeasurementSamplingProtocols2 = No lipids extracted, uncorrected #Maximum uploadable media size in megabytes (MB) #This value is used for encounter images and videos as well as for file associations added to a MarkedIndividual. -maxMediaSize = 3 -#Maximum number of media per encounter -maximumMediaCountEncounter = 200 +maxMediaSize = 40 #allow video download for not logged in users videoDLNotLoggedIn = false diff --git a/devops/deploy/docker-compose.yml b/devops/deploy/docker-compose.yml index fce80da16c..573ea4c22e 100644 --- a/devops/deploy/docker-compose.yml +++ b/devops/deploy/docker-compose.yml @@ -2,10 +2,13 @@ services: db: image: postgres:13.4 healthcheck: - test: [ "CMD-SHELL", "pg_isready -U postgres || exit 1" ] - interval: 10s - timeout: 5s - retries: 60 + # More robust check: verify PostgreSQL can actually execute a query + # pg_isready only checks if accepting connections, not if responsive + test: [ "CMD-SHELL", "pg_isready -U ${WILDBOOK_DB_USER:-wildbook} -d ${WILDBOOK_DB_NAME:-wildbook} && psql -U ${WILDBOOK_DB_USER:-wildbook} -d ${WILDBOOK_DB_NAME:-wildbook} -c 'SELECT 1' || exit 1" ] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s labels: - autoheal=true user: postgres @@ -74,10 +77,13 @@ services: opensearch: image: opensearchproject/opensearch:2.15.0 healthcheck: - test: [ "CMD-SHELL", "curl --silent --fail 127.0.0.1:9200/_cluster/health || exit 1" ] - interval: 10s - timeout: 5s - retries: 60 + # Check cluster health and verify status is not "red" + # Green = all shards allocated, Yellow = primary shards ok, Red = cluster down + test: [ "CMD-SHELL", "curl --silent --fail 127.0.0.1:9200/_cluster/health | grep -qE '\"status\":\"(green|yellow)\"' || exit 1" ] + interval: 30s + timeout: 10s + retries: 3 + start_period: 120s labels: - autoheal=true volumes: @@ -108,6 +114,15 @@ services: # TODO dkim and spf needs to be added/supported smtp: image: boky/postfix + healthcheck: + # Check if postfix master process is running + test: [ "CMD-SHELL", "pgrep -x master || exit 1" ] + interval: 60s + timeout: 10s + retries: 3 + start_period: 30s + labels: + - autoheal=true networks: - intranet ports: @@ -125,11 +140,14 @@ services: image: nginx:1.23.4 depends_on: - wildbook - #healthcheck: - #test: [ "CMD", "curl", "-f", "http://localhost:84/"] - #interval: 10s - #timeout: 5s - #retries: 60 + healthcheck: + # Check if nginx master process is running and can accept connections + # nginx:1.23.4 doesn't include curl, so we check the pid file and use nginx -t + test: [ "CMD-SHELL", "nginx -t 2>/dev/null && kill -0 $(cat /var/run/nginx.pid 2>/dev/null) || exit 1" ] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s labels: - autoheal=true volumes: diff --git a/devops/development/.dockerfiles/tomcat/commonConfiguration.properties b/devops/development/.dockerfiles/tomcat/commonConfiguration.properties index a38250d6c9..6e6e1a13f8 100644 --- a/devops/development/.dockerfiles/tomcat/commonConfiguration.properties +++ b/devops/development/.dockerfiles/tomcat/commonConfiguration.properties @@ -329,10 +329,7 @@ biologicalMeasurementSamplingProtocols2 = No lipids extracted, uncorrected #Maximum uploadable media size in megabytes (MB) #This value is used for encounter images and videos as well as for file associations added to a MarkedIndividual. -# Refer to PhotoUploadDevNotes.md for information/where else to change -maxMediaSize = 3 -#Maximum number of media per encounter -maximumMediaCountEncounter = 200 +maxMediaSize = 40 #allow video download for not logged in users videoDLNotLoggedIn = false diff --git a/frontend/src/__tests__/pages/BulkImport/BulkImportTask.test.js b/frontend/src/__tests__/pages/BulkImport/BulkImportTask.test.js index 89f88cff3e..01117cbe40 100644 --- a/frontend/src/__tests__/pages/BulkImport/BulkImportTask.test.js +++ b/frontend/src/__tests__/pages/BulkImport/BulkImportTask.test.js @@ -112,6 +112,7 @@ describe("BulkImportTask", () => { expect( screen.getByText("BULK_IMPORT_TASK_STATUS_in_progress"), ).toBeInTheDocument(); + expect(screen.getByText("RESULTS_PER_PAGE")).toBeInTheDocument(); expect(screen.getByTestId("simple-table")).toBeInTheDocument(); expect(screen.getByText("E123")).toBeInTheDocument(); }); diff --git a/frontend/src/components/AttributesAndValueComponent.jsx b/frontend/src/components/AttributesAndValueComponent.jsx index 0c46dc45ee..ef83949102 100644 --- a/frontend/src/components/AttributesAndValueComponent.jsx +++ b/frontend/src/components/AttributesAndValueComponent.jsx @@ -1,13 +1,14 @@ -import React from 'react'; -import { FormattedMessage } from 'react-intl'; +import React from "react"; +import { FormattedMessage } from "react-intl"; export const AttributesAndValueComponent = ({ attributeId, value }) => { - return ( -
-
- {":"} -
-

{value}

-
- ); -} \ No newline at end of file + return ( +
+
+ + {":"} +
+

{value}

+
+ ); +}; diff --git a/frontend/src/components/Divider.jsx b/frontend/src/components/Divider.jsx index 8c67337a6a..9679cffe4f 100644 --- a/frontend/src/components/Divider.jsx +++ b/frontend/src/components/Divider.jsx @@ -1,13 +1,15 @@ -import React from 'react'; +import React from "react"; export const Divider = () => { - return ( -
- ); -} \ No newline at end of file + return ( +
+ ); +}; diff --git a/frontend/src/components/FollowUpSection.jsx b/frontend/src/components/FollowUpSection.jsx index 339c045ab0..894a5d204c 100644 --- a/frontend/src/components/FollowUpSection.jsx +++ b/frontend/src/components/FollowUpSection.jsx @@ -30,7 +30,7 @@ export const FollowUpSection = observer(({ store }) => { store.setSubmitterName(e.target.value); }} value={store.followUpSection?.submitter?.name} - /> + > @@ -39,20 +39,13 @@ export const FollowUpSection = observer(({ store }) => { { store.setSubmitterEmail(e.target.value); - if (store.followUpSection?.submitter?.emailError) { - store.setSubmitterEmailError(false); - } }} value={store.followUpSection?.submitter?.email} - isInvalid={store.followUpSection?.submitter?.emailError} - /> - - - + > @@ -73,7 +66,7 @@ export const FollowUpSection = observer(({ store }) => { store.setPhotographerName(e.target.value); }} value={store.followUpSection?.photographer?.name} - /> + > @@ -82,20 +75,13 @@ export const FollowUpSection = observer(({ store }) => { { store.setPhotographerEmail(e.target.value); - if (store.followUpSection?.photographer?.emailError) { - store.setPhotographerEmailError(false); - } }} value={store.followUpSection?.photographer?.email} - isInvalid={store.followUpSection?.photographer?.emailError} - /> - - - + > @@ -114,16 +100,9 @@ export const FollowUpSection = observer(({ store }) => { placeholder={intl.formatMessage({ id: "CSL_EMAILS_EXAMPLE" })} onChange={(e) => { store.setAdditionalEmails(e.target.value); - if (store.followUpSection?.additionalEmailsError) { - store.setAdditionalEmailsError(false); - } }} value={store.followUpSection?.additionalEmails} - isInvalid={store.followUpSection?.additionalEmailsError} /> - - - ); diff --git a/frontend/src/components/IdentifyIcon.jsx b/frontend/src/components/IdentifyIcon.jsx index 04a59b9fcd..a8c6a1d6d2 100644 --- a/frontend/src/components/IdentifyIcon.jsx +++ b/frontend/src/components/IdentifyIcon.jsx @@ -2,21 +2,31 @@ import React from "react"; import ThemeColorContext from "../ThemeColorProvider"; export default function IdentifyIcon() { - const theme = React.useContext(ThemeColorContext); - return ( -
- - - -
- ); -} \ No newline at end of file + const theme = React.useContext(ThemeColorContext); + return ( +
+ + + +
+ ); +} diff --git a/frontend/src/components/InactivePill.jsx b/frontend/src/components/InactivePill.jsx index d6a37e2fde..3142f36c22 100644 --- a/frontend/src/components/InactivePill.jsx +++ b/frontend/src/components/InactivePill.jsx @@ -1,5 +1,3 @@ - - import React from "react"; import PropTypes from "prop-types"; import ThemeColorContext from "../ThemeColorProvider"; @@ -16,7 +14,7 @@ export default function InactivePill({ text, onClick, style }) { color: "#000", borderRadius: "20px", cursor: "pointer", - ...style, + ...style, }} > {text} @@ -26,4 +24,4 @@ export default function InactivePill({ text, onClick, style }) { InactivePill.propTypes = { text: PropTypes.string.isRequired, onClick: PropTypes.func.isRequired, -}; \ No newline at end of file +}; diff --git a/frontend/src/components/MainButton.jsx b/frontend/src/components/MainButton.jsx index 78b006136b..973f8c207f 100644 --- a/frontend/src/components/MainButton.jsx +++ b/frontend/src/components/MainButton.jsx @@ -14,8 +14,8 @@ export default function MainButton({ className = "", children, style, - noArrow, - target = false, // If true, opens link in a new tab + noArrow, + target = false, // If true, opens link in a new tab ...rest }) { const [isHovered, setIsHovered] = useState(false); @@ -56,6 +56,7 @@ export default function MainButton({ target={target ? "_blank" : "_self"} className="d-flex align-items-center justify-content-center text-decoration-none w-100 h-100" style={{ color: "inherit" }} + rel="noreferrer" > {children} diff --git a/frontend/src/components/Pill.jsx b/frontend/src/components/Pill.jsx index 75a5c8450f..ce7e881b19 100644 --- a/frontend/src/components/Pill.jsx +++ b/frontend/src/components/Pill.jsx @@ -19,7 +19,7 @@ export default function Pill({ text, onClick, style, active }) { ...style, }} > - {} + {} ); } diff --git a/frontend/src/components/PillWithButton.jsx b/frontend/src/components/PillWithButton.jsx index f6f62e9f91..fb4b0a694c 100644 --- a/frontend/src/components/PillWithButton.jsx +++ b/frontend/src/components/PillWithButton.jsx @@ -1,17 +1,13 @@ - // PillWithButton.jsx import React from "react"; import ThemeColorContext from "../ThemeColorProvider"; -export default function PillWithButton({ - text, - onClick, - onClose, -}) { +export default function PillWithButton({ text, onClick, onClose }) { const theme = React.useContext(ThemeColorContext); return ( -
{text} @@ -41,4 +38,3 @@ export default function PillWithButton({
); } - diff --git a/frontend/src/components/SimpleDataTable.jsx b/frontend/src/components/SimpleDataTable.jsx index e70edf3b29..3a34621c22 100644 --- a/frontend/src/components/SimpleDataTable.jsx +++ b/frontend/src/components/SimpleDataTable.jsx @@ -20,11 +20,15 @@ const SimpleDataTable = ({ columns = [], data = [], perPage = 10 }) => { const pageCount = Math.ceil(data.length / perPage); + useEffect(() => { + setCurrentPage(0); + }, [perPage]); + useEffect(() => { const start = currentPage * perPage; const end = start + perPage; setPagedData(data.slice(start, end)); - }, [data, currentPage, perPage]); + }, [data, currentPage]); const userColumns = columns.map((col) => ({ id: col.selector, @@ -68,6 +72,8 @@ const SimpleDataTable = ({ columns = [], data = [], perPage = 10 }) => { customStyles={customStyles} conditionalRowStyles={conditionalRowStyles} highlightOnHover + fixedHeader + fixedHeaderScrollHeight="85vh" /> diff --git a/frontend/src/components/ToolTip.jsx b/frontend/src/components/ToolTip.jsx index eb3bb56d53..275f407970 100644 --- a/frontend/src/components/ToolTip.jsx +++ b/frontend/src/components/ToolTip.jsx @@ -18,7 +18,7 @@ function Tooltip({ show, x, y, children }) { fontSize: 12, lineHeight: 1.4, zIndex: 9999, - whiteSpace: "pre-line", + whiteSpace: "pre-line", }} > {children} @@ -26,4 +26,4 @@ function Tooltip({ show, x, y, children }) { ); } -export default Tooltip; \ No newline at end of file +export default Tooltip; diff --git a/frontend/src/components/filterFields/DateFilter.jsx b/frontend/src/components/filterFields/DateFilter.jsx index 79dc32a0ab..3f9f5a26ca 100644 --- a/frontend/src/components/filterFields/DateFilter.jsx +++ b/frontend/src/components/filterFields/DateFilter.jsx @@ -2,13 +2,8 @@ import React, { useState, useEffect } from "react"; import { FormattedMessage } from "react-intl"; import Description from "../Form/Description"; import FormGroupMultiSelect from "../Form/FormGroupMultiSelect"; -import { FormLabel, FormGroup } from "react-bootstrap"; +import { FormLabel, FormGroup, FormControl } from "react-bootstrap"; import { observer } from "mobx-react-lite"; -import { DatePicker } from "antd"; -import dayjs from "dayjs"; - -const FMT = "YYYY-MM-DD"; -const toDayjs = (s) => (s ? dayjs(s) : null); const DateFilter = observer(({ data, store }) => { const [startDate, setStartDate] = useState(""); @@ -100,45 +95,34 @@ const DateFilter = observer(({ data, store }) => {

- f.filterId === "date") - ?.query?.range?.date?.gte?.split("T")[0] || startDate, - )} - format={FMT} - placeholder={FMT} - allowClear - onChange={(d) => { - const iso = d ? d.format(FMT) : ""; - setStartDate(iso); + .find((filter) => filter.filterId === "date") + ?.query?.range?.date?.gte?.split("T")[0] || startDate + } + onChange={(e) => { + setStartDate(e.target.value); updateQuery1(); }} - getPopupContainer={(trigger) => trigger.parentElement} /> -

- f.filterId === "date") - ?.query?.range?.date?.lte?.split("T")[0] || endDate, - )} - format={FMT} - placeholder={FMT} - allowClear - onChange={(d) => { - const iso = d ? d.format(FMT) : ""; - setEndDate(iso); + .find((filter) => filter.filterId === "date") + ?.query?.range?.date?.lte?.split("T")[0] || endDate + } + onChange={(e) => { + setEndDate(e.target.value); updateQuery1(); }} - getPopupContainer={(trigger) => trigger.parentElement} />
@@ -156,54 +140,47 @@ const DateFilter = observer(({ data, store }) => { /> <> - - - +

+ + + +

+

- filter.filterId === "dateSubmitted") ?.query?.range?.dateSubmitted?.gte?.split("T")[0] || - submissionStartDate, - )} - format={FMT} - placeholder={FMT} - allowClear - onChange={(d) => { - const iso = d ? d.format(FMT) : ""; + submissionStartDate + } + onChange={(e) => { updateQuery2(); - setSubmissionStartDate(iso); + setSubmissionStartDate(e.target.value); }} - getPopupContainer={(trigger) => trigger.parentElement} />

- filter.filterId === "dateSubmitted") ?.query?.range?.dateSubmitted?.lte?.split("T")[0] || - submissionEndDate, - )} - format={FMT} - placeholder={FMT} - allowClear - onChange={(d) => { - const iso = d ? d.format(FMT) : ""; + submissionEndDate + } + onChange={(e) => { updateQuery2(); - setSubmissionEndDate(iso); + setSubmissionEndDate(e.target.value); }} - getPopupContainer={(trigger) => trigger.parentElement} />
diff --git a/frontend/src/components/filterFields/IndividualDateFilter.jsx b/frontend/src/components/filterFields/IndividualDateFilter.jsx index 699560f213..ef4f363d9d 100644 --- a/frontend/src/components/filterFields/IndividualDateFilter.jsx +++ b/frontend/src/components/filterFields/IndividualDateFilter.jsx @@ -1,13 +1,8 @@ import React from "react"; import { FormattedMessage } from "react-intl"; import Description from "../Form/Description"; -import { FormGroup } from "react-bootstrap"; +import { FormGroup, FormControl } from "react-bootstrap"; import { observer } from "mobx-react-lite"; -import { DatePicker } from "antd"; -import dayjs from "dayjs"; - -const FMT = "YYYY-MM-DD"; -const toDayjs = (s) => (s ? dayjs(s) : null); const IndividualDateFilter = observer(({ store }) => { return ( @@ -23,27 +18,23 @@ const IndividualDateFilter = observer(({ store }) => {

- filter.filterId === "individualTimeOfBirth") - ?.query?.range?.individualTimeOfBirth?.gte?.split("T")[0] || "", - )} - format={FMT} - placeholder={FMT} - allowClear - onChange={(d) => { - const iso = d ? d.format(FMT) : ""; - if (iso) { + ?.query?.range?.individualTimeOfBirth?.gte.split("T")[0] || "" + } + onChange={(e) => { + if (e.target.value) { store.addFilter( "individualTimeOfBirth", "filter", { range: { individualTimeOfBirth: { - gte: `${iso}T00:00:00.000Z`, - lte: `${iso}T23:59:59.000Z`, + gte: `${e.target.value}T00:00:00.000Z`, + lte: `${e.target.value}T23:59:59.000Z`, }, }, }, @@ -53,35 +44,29 @@ const IndividualDateFilter = observer(({ store }) => { store.removeFilter("individualTimeOfBirth"); } }} - getPopupContainer={(trigger) => trigger.parentElement} />

- - filter.filterId === "individualTimeOfDeath") - ?.query?.range?.individualTimeOfDeath?.gte?.split("T")[0] || "", - )} - format={FMT} - placeholder={FMT} - allowClear - onChange={(d) => { - const iso = d ? d.format(FMT) : ""; - if (iso) { + ?.query?.range?.individualTimeOfDeath?.gte.split("T")[0] || "" + } + onChange={(e) => { + if (e.target.value) { store.addFilter( "individualTimeOfDeath", "filter", { range: { individualTimeOfDeath: { - gte: `${iso}T00:00:00.000Z`, - lte: `${iso}T23:59:59.000Z`, + gte: `${e.target.value}T00:00:00.000Z`, + lte: `${e.target.value}T23:59:59.000Z`, }, }, }, @@ -91,7 +76,6 @@ const IndividualDateFilter = observer(({ store }) => { store.removeFilter("individualTimeOfDeath"); } }} - getPopupContainer={(trigger) => trigger.parentElement} />
diff --git a/frontend/src/components/generalInputs/CoordinatesInput.jsx b/frontend/src/components/generalInputs/CoordinatesInput.jsx index 8928f00178..994053db12 100644 --- a/frontend/src/components/generalInputs/CoordinatesInput.jsx +++ b/frontend/src/components/generalInputs/CoordinatesInput.jsx @@ -55,7 +55,7 @@ export const CoordinatesInput = observer(({ store }) => { return () => { window.google.maps.removeListener(clickListener); - } + }; }) .catch((error) => { console.error("Error loading Google Maps", error); @@ -89,14 +89,14 @@ export const CoordinatesInput = observer(({ store }) => { if (!store.lat || !store.lon) return; if (store.lat) { store.setFieldValue("location", "locationGeoPoint", { - ...store.getFieldValue("location","locationGeoPoint") || {}, + ...(store.getFieldValue("location", "locationGeoPoint") || {}), lat: store.lat, }); } if (store.lon) { store.setFieldValue("location", "locationGeoPoint", { - ...store.getFieldValue("location","locationGeoPoint") || {}, + ...(store.getFieldValue("location", "locationGeoPoint") || {}), lon: store.lon, }); } @@ -120,14 +120,14 @@ export const CoordinatesInput = observer(({ store }) => { onChange={(e) => { let newLat = e.target.value; setPan(true); - store.setLat(newLat); + store.setLat(newLat); }} /> {store.errors.getFieldError("location", "latitude") && (
{store.errors.getFieldError("location", "latitude") || ""}
- )} + )}
{ onChange={(e) => { const newLon = e.target.value; setPan(true); - store.setLon(newLon); + store.setLon(newLon); }} /> {store.errors.getFieldError("location", "longitude") && ( diff --git a/frontend/src/components/icons/AttributesIcon.jsx b/frontend/src/components/icons/AttributesIcon.jsx index 0c49776890..a1ed69b83f 100644 --- a/frontend/src/components/icons/AttributesIcon.jsx +++ b/frontend/src/components/icons/AttributesIcon.jsx @@ -2,21 +2,31 @@ import React from "react"; import ThemeColorContext from "../../ThemeColorProvider"; export default function AttributesIcon() { - const theme = React.useContext(ThemeColorContext); - return ( -
- - - -
- ); -} \ No newline at end of file + const theme = React.useContext(ThemeColorContext); + return ( +
+ + + +
+ ); +} diff --git a/frontend/src/components/icons/ContactIcon.jsx b/frontend/src/components/icons/ContactIcon.jsx index ceed86e7fa..b3db21919c 100644 --- a/frontend/src/components/icons/ContactIcon.jsx +++ b/frontend/src/components/icons/ContactIcon.jsx @@ -1,11 +1,18 @@ - -import React from 'react'; +import React from "react"; export default function ContactIcon() { - return ( - - - - - ); -} \ No newline at end of file + return ( + + + + ); +} diff --git a/frontend/src/components/icons/DateIcon.jsx b/frontend/src/components/icons/DateIcon.jsx index bf42eee247..0a7cfe3806 100644 --- a/frontend/src/components/icons/DateIcon.jsx +++ b/frontend/src/components/icons/DateIcon.jsx @@ -2,21 +2,31 @@ import React from "react"; import ThemeColorContext from "../../ThemeColorProvider"; export default function DateIcon() { - const theme = React.useContext(ThemeColorContext); - return ( -
- - - -
- ); -} \ No newline at end of file + const theme = React.useContext(ThemeColorContext); + return ( +
+ + + +
+ ); +} diff --git a/frontend/src/components/icons/EditIcon.jsx b/frontend/src/components/icons/EditIcon.jsx index e93ade2d37..42900874c9 100644 --- a/frontend/src/components/icons/EditIcon.jsx +++ b/frontend/src/components/icons/EditIcon.jsx @@ -1,14 +1,20 @@ import React from "react"; -import ThemeColorContext from "../../ThemeColorProvider"; export default function EditIcon() { - const theme = React.useContext(ThemeColorContext); - return ( -
- - - -
- ); -} \ No newline at end of file + return ( +
+ + + +
+ ); +} diff --git a/frontend/src/components/icons/HistoryIcon.jsx b/frontend/src/components/icons/HistoryIcon.jsx index 21d103b232..9143d6f123 100644 --- a/frontend/src/components/icons/HistoryIcon.jsx +++ b/frontend/src/components/icons/HistoryIcon.jsx @@ -1,10 +1,18 @@ -import React from 'react'; +import React from "react"; export default function HistoryIcon() { - return ( - - - - - ); -} \ No newline at end of file + return ( + + + + ); +} diff --git a/frontend/src/components/icons/ImageIcon.jsx b/frontend/src/components/icons/ImageIcon.jsx index 071c4ec44c..22f16929fa 100644 --- a/frontend/src/components/icons/ImageIcon.jsx +++ b/frontend/src/components/icons/ImageIcon.jsx @@ -2,22 +2,35 @@ import React from "react"; import ThemeColorContext from "../../ThemeColorProvider"; export default function ImageIcon() { - const theme = React.useContext(ThemeColorContext); - return ( -
- - - - -
- ); -} \ No newline at end of file + const theme = React.useContext(ThemeColorContext); + return ( +
+ + + + +
+ ); +} diff --git a/frontend/src/components/icons/LocationIcon.jsx b/frontend/src/components/icons/LocationIcon.jsx index b14bec880f..00d00fc1ad 100644 --- a/frontend/src/components/icons/LocationIcon.jsx +++ b/frontend/src/components/icons/LocationIcon.jsx @@ -2,21 +2,31 @@ import React from "react"; import ThemeColorContext from "../../ThemeColorProvider"; export default function LocationIcon() { - const theme = React.useContext(ThemeColorContext); - return ( -
- - - -
- ); -} \ No newline at end of file + const theme = React.useContext(ThemeColorContext); + return ( +
+ + + +
+ ); +} diff --git a/frontend/src/components/icons/MailIcon.jsx b/frontend/src/components/icons/MailIcon.jsx index fb6b86da40..f2c0fe7192 100644 --- a/frontend/src/components/icons/MailIcon.jsx +++ b/frontend/src/components/icons/MailIcon.jsx @@ -2,21 +2,31 @@ import React from "react"; import ThemeColorContext from "../../ThemeColorProvider"; export default function MailIcon() { - const theme = React.useContext(ThemeColorContext); - return ( -
- - - -
- ); -} \ No newline at end of file + const theme = React.useContext(ThemeColorContext); + return ( +
+ + + +
+ ); +} diff --git a/frontend/src/components/icons/MeasurementsIcon.jsx b/frontend/src/components/icons/MeasurementsIcon.jsx index 92c589d819..cdb6fe4122 100644 --- a/frontend/src/components/icons/MeasurementsIcon.jsx +++ b/frontend/src/components/icons/MeasurementsIcon.jsx @@ -2,21 +2,31 @@ import React from "react"; import ThemeColorContext from "../../ThemeColorProvider"; export default function MeasurementsIcon() { - const theme = React.useContext(ThemeColorContext); - return ( -
- - - -
- ); -} \ No newline at end of file + const theme = React.useContext(ThemeColorContext); + return ( +
+ + + +
+ ); +} diff --git a/frontend/src/components/icons/MetaDataIcon.jsx b/frontend/src/components/icons/MetaDataIcon.jsx index 244b3af842..7ac758bb88 100644 --- a/frontend/src/components/icons/MetaDataIcon.jsx +++ b/frontend/src/components/icons/MetaDataIcon.jsx @@ -2,21 +2,31 @@ import React from "react"; import ThemeColorContext from "../../ThemeColorProvider"; export default function MetadataIcon() { - const theme = React.useContext(ThemeColorContext); - return ( -
- - - -
- ); -} \ No newline at end of file + const theme = React.useContext(ThemeColorContext); + return ( +
+ + + +
+ ); +} diff --git a/frontend/src/components/icons/PencilIcon.jsx b/frontend/src/components/icons/PencilIcon.jsx index 4797a32123..66bd277c56 100644 --- a/frontend/src/components/icons/PencilIcon.jsx +++ b/frontend/src/components/icons/PencilIcon.jsx @@ -1,6 +1,6 @@ import React from "react"; -export default function PencilIcon({styles = {}}) { +export default function PencilIcon({ styles = {} }) { return (
- - - -
- ); -} \ No newline at end of file + const theme = React.useContext(ThemeColorContext); + return ( +
+ + + +
+ ); +} diff --git a/frontend/src/components/icons/ProjectsIcon.jsx b/frontend/src/components/icons/ProjectsIcon.jsx index 2940c52f21..5f1ffc1080 100644 --- a/frontend/src/components/icons/ProjectsIcon.jsx +++ b/frontend/src/components/icons/ProjectsIcon.jsx @@ -1,24 +1,33 @@ import React from "react"; import ThemeColorContext from "../../ThemeColorProvider"; -export default function ProjectsIcon({className}) { - const theme = React.useContext(ThemeColorContext); - return ( -
- - - -
- ); -} \ No newline at end of file +export default function ProjectsIcon({ className }) { + const theme = React.useContext(ThemeColorContext); + return ( +
+ + + +
+ ); +} diff --git a/frontend/src/components/icons/RemoveIcon.jsx b/frontend/src/components/icons/RemoveIcon.jsx index 25c0e3d541..ce764096f1 100644 --- a/frontend/src/components/icons/RemoveIcon.jsx +++ b/frontend/src/components/icons/RemoveIcon.jsx @@ -1,19 +1,29 @@ import React from "react"; export default function RemoveIcon() { - return ( -
- - - -
- ); -} \ No newline at end of file + return ( +
+ + + +
+ ); +} diff --git a/frontend/src/components/icons/TrackingIcon.jsx b/frontend/src/components/icons/TrackingIcon.jsx index d98573cf48..224ab6eb1e 100644 --- a/frontend/src/components/icons/TrackingIcon.jsx +++ b/frontend/src/components/icons/TrackingIcon.jsx @@ -2,21 +2,31 @@ import React from "react"; import ThemeColorContext from "../../ThemeColorProvider"; export default function TrackingIcon() { - const theme = React.useContext(ThemeColorContext); - return ( -
- - - -
- ); -} \ No newline at end of file + const theme = React.useContext(ThemeColorContext); + return ( +
+ + + +
+ ); +} diff --git a/frontend/src/components/icons/TrashCanIcon.jsx b/frontend/src/components/icons/TrashCanIcon.jsx index 96e066ee89..9b5f0f3830 100644 --- a/frontend/src/components/icons/TrashCanIcon.jsx +++ b/frontend/src/components/icons/TrashCanIcon.jsx @@ -1,7 +1,6 @@ - import React from "react"; -export default function TrashcanIcon({styles = {}}) { +export default function TrashcanIcon({ styles = {} }) { return (
- - - + + +
); } diff --git a/frontend/src/constants/navMenu.js b/frontend/src/constants/navMenu.js index 6d20ce5f61..75116883c1 100644 --- a/frontend/src/constants/navMenu.js +++ b/frontend/src/constants/navMenu.js @@ -228,15 +228,6 @@ const authenticatedMenu = ( }, { Administer: [ - { - name: ( - - ), - href: "/myUsers.jsp", - }, { name: ( { e.preventDefault(); @@ -34,13 +30,11 @@ export const BulkImportImageUpload = observer(({ store, renderMode1 }) => { const theme = useContext(ThemeContext); const originalBorder = `1px dashed ${theme.primaryColors.primary500}`; const { data } = useGetSiteSettings(); - const maxSize = data?.maximumMediaSizeMegabytes || defaultMaxMediaSize; + const maxSize = data?.maximumMediaSizeMegabytes || 40; const [isProcessingDrop, setIsProcessingDrop] = useState(false); const [renderMode, setRenderMode] = useState("grid"); const THUMBNAIL_THRESHOLD = 50; - store.setMaxImageCount( - data?.maximumMediaCountEncounter || defaultMaxMediaCount, - ); + store.setMaxImageCount(data?.maximumMediaCountEncounter || 200); const currentCount = store.imagePreview.length; useEffect(() => { diff --git a/frontend/src/pages/BulkImport/BulkImportInstructionsModal.jsx b/frontend/src/pages/BulkImport/BulkImportInstructionsModal.jsx index 6d7665b59e..ce483974a8 100644 --- a/frontend/src/pages/BulkImport/BulkImportInstructionsModal.jsx +++ b/frontend/src/pages/BulkImport/BulkImportInstructionsModal.jsx @@ -4,19 +4,9 @@ import { FormattedMessage } from "react-intl"; import { observer } from "mobx-react-lite"; import MainButton from "../../components/MainButton"; import ThemeColorContext from "../../ThemeColorProvider"; -import useGetSiteSettings from "../../models/useGetSiteSettings"; -import { - defaultMaxMediaSize, - defaultMaxMediaCount, -} from "../../constants/photoUpload.js"; const BulkImportInstructionsModal = observer(({ store }) => { const theme = React.useContext(ThemeColorContext); - const { data } = useGetSiteSettings(); - const maxSize = data?.maximumMediaSizeMegabytes || defaultMaxMediaSize; - const maxImageCount = - data?.maximumMediaCountEncounter || defaultMaxMediaCount; - return ( {
  • - +
  • - +
  • diff --git a/frontend/src/pages/BulkImport/BulkImportStore.js b/frontend/src/pages/BulkImport/BulkImportStore.js index 6423758fe6..c055d6c93f 100644 --- a/frontend/src/pages/BulkImport/BulkImportStore.js +++ b/frontend/src/pages/BulkImport/BulkImportStore.js @@ -116,43 +116,14 @@ export class BulkImportStore { "Encounter.year": { required: true, validate: (val) => { - if (val == null) return false; - const FLEXIBLE_DATE_RE = - /^(\d{4})(?:-(\d{1,2})(?:-(\d{1,2})(?:T(\d{1,2})(?::(\d{1,2}))?)?)?)?$/; - - const str = String(val).trim(); - if (!str) return false; - - const m = str.match(FLEXIBLE_DATE_RE); - if (!m) { - return false; - } - - const [, year, month, day, hour, minute] = m; - - let normalized = year; - let format = "YYYY"; - - if (month !== undefined) { - normalized += "-" + month.padStart(2, "0"); - format += "-MM"; - } - - if (day !== undefined) { - normalized += "-" + day.padStart(2, "0"); - format += "-DD"; - } - - if (hour !== undefined) { - normalized += "T" + hour.padStart(2, "0"); - format += "[T]HH"; - } - - if (minute !== undefined) { - normalized += ":" + minute.padStart(2, "0"); - format += ":mm"; - } - const parsed = dayjs(normalized, format, true); + const FORMATS = [ + "YYYY", + "YYYY-MM", + "YYYY-MM-DD", + "YYYY-MM-DDTHH", + "YYYY-MM-DDTHH:mm", + ]; + const parsed = dayjs(val, FORMATS, true); return parsed.isValid() && !parsed.isAfter(dayjs()); }, message: "BULKIMPORT_ERROR_INVALID_DATEFORMAT", diff --git a/frontend/src/pages/BulkImport/BulkImportTask.jsx b/frontend/src/pages/BulkImport/BulkImportTask.jsx index 7d2aa2fcac..19c4eb29a2 100644 --- a/frontend/src/pages/BulkImport/BulkImportTask.jsx +++ b/frontend/src/pages/BulkImport/BulkImportTask.jsx @@ -33,6 +33,7 @@ const BulkImportTask = observer(() => { const { task, isLoading, error, refetch } = useGetBulkImportTask(taskId); const { data: siteData } = useGetSiteSettings(); const [userRoles, setUserRoles] = useState(null); + const [rowsPerPage, setRowsPerPage] = useState(10); const store = useLocalObservable(() => new BulkImportTaskStore()); const previousLocationID = task?.matchingLocations || []; @@ -405,7 +406,34 @@ const BulkImportTask = observer(() => { />
    - +
    + +
    + + diff --git a/frontend/src/pages/Encounter/AttributesSectionEdit.jsx b/frontend/src/pages/Encounter/AttributesSectionEdit.jsx index e4f0a8d8ef..0938e79cd1 100644 --- a/frontend/src/pages/Encounter/AttributesSectionEdit.jsx +++ b/frontend/src/pages/Encounter/AttributesSectionEdit.jsx @@ -1,122 +1,81 @@ -import React from 'react'; -import { observer } from 'mobx-react-lite'; -import SelectInput from '../../components/generalInputs/SelectInput'; +import React from "react"; +import { observer } from "mobx-react-lite"; +import SelectInput from "../../components/generalInputs/SelectInput"; import { Alert } from "react-bootstrap"; -import TextInput from '../../components/generalInputs/TextInput'; -import FreeTextAndSelectInput from '../../components/generalInputs/FreeTextAndSelectInput'; +import TextInput from "../../components/generalInputs/TextInput"; +import FreeTextAndSelectInput from "../../components/generalInputs/FreeTextAndSelectInput"; export const AttributesSectionEdit = observer(({ store }) => { - return
    - { - store.setFieldValue("attributes", "taxonomy", v); - }} - options={store.taxonomyOptions} - className="mb-3" - /> - - store.setFieldValue("attributes", "livingStatus", v) - } - options={store.livingStatusOptions} - className="mb-3" - /> - - store.setFieldValue("attributes", "sex", v) - } - options={store.sexOptions} - className="mb-3" - /> - - store.setFieldValue( - "attributes", - "distinguishingScar", - v, - ) - } - /> - - store.setFieldValue("attributes", "behavior", v) - } - options={store.behaviorOptions} - className="mb-3" - /> - - store.setFieldValue("attributes", "groupRole", v) - } - /> - - store.setFieldValue("attributes", "patterningCode", v) - } - options={store.patterningCodeOptions} - className="mb-3" - /> - - store.setFieldValue("attributes", "lifeStage", v) - } - options={store.lifeStageOptions} - className="mb-3" - /> - - store.setFieldValue( - "attributes", - "occurrenceRemarks", - v, - ) - } - /> - {store.errors.hasSectionError("attributes") && ( - - {store.errors.getSectionErrors("attributes").join(";")} - - )} + return ( +
    + { + store.setFieldValue("attributes", "taxonomy", v); + }} + options={store.taxonomyOptions} + className="mb-3" + /> + store.setFieldValue("attributes", "livingStatus", v)} + options={store.livingStatusOptions} + className="mb-3" + /> + store.setFieldValue("attributes", "sex", v)} + options={store.sexOptions} + className="mb-3" + /> + + store.setFieldValue("attributes", "distinguishingScar", v) + } + /> + store.setFieldValue("attributes", "behavior", v)} + options={store.behaviorOptions} + className="mb-3" + /> + store.setFieldValue("attributes", "groupRole", v)} + /> + store.setFieldValue("attributes", "patterningCode", v)} + options={store.patterningCodeOptions} + className="mb-3" + /> + store.setFieldValue("attributes", "lifeStage", v)} + options={store.lifeStageOptions} + className="mb-3" + /> + + store.setFieldValue("attributes", "occurrenceRemarks", v) + } + /> + {store.errors.hasSectionError("attributes") && ( + + {store.errors.getSectionErrors("attributes").join(";")} + + )}
    -}) + ); +}); diff --git a/frontend/src/pages/Encounter/AttributesSectionReview.jsx b/frontend/src/pages/Encounter/AttributesSectionReview.jsx index c77cfd643c..6b737af375 100644 --- a/frontend/src/pages/Encounter/AttributesSectionReview.jsx +++ b/frontend/src/pages/Encounter/AttributesSectionReview.jsx @@ -1,44 +1,46 @@ -import React from 'react'; -import { observer } from 'mobx-react-lite'; -import { AttributesAndValueComponent } from '../../components/AttributesAndValueComponent'; +import React from "react"; +import { observer } from "mobx-react-lite"; +import { AttributesAndValueComponent } from "../../components/AttributesAndValueComponent"; export const AttributesSectionReview = observer(({ store }) => { - return
    - - - - - - - - - + return ( +
    + + + + + + + + +
    -}) + ); +}); diff --git a/frontend/src/pages/Encounter/DateSectionEdit.jsx b/frontend/src/pages/Encounter/DateSectionEdit.jsx index a01c5ddb21..730b2440db 100644 --- a/frontend/src/pages/Encounter/DateSectionEdit.jsx +++ b/frontend/src/pages/Encounter/DateSectionEdit.jsx @@ -1,37 +1,35 @@ -import React from 'react'; -import { observer } from 'mobx-react-lite'; -import DateInput from '../../components/generalInputs/DateInput'; -import TextInput from '../../components/generalInputs/TextInput'; +import React from "react"; +import { observer } from "mobx-react-lite"; +import DateInput from "../../components/generalInputs/DateInput"; +import TextInput from "../../components/generalInputs/TextInput"; import { Alert } from "react-bootstrap"; export const DateSectionEdit = observer(({ store }) => { - return
    - { - store.setFieldValue("date", "date", v); - }} - className="mb-3" - /> - {store.errors.getFieldError("date", "date") && ( -
    - {store.errors.getFieldError("date", "date")} -
    - )} - - store.setFieldValue("date", "verbatimEventDate", v) - } - /> - {store.errors.hasSectionError("date") && ( - - {store.errors.getSectionErrors("date").join(";")} - - )} + return ( +
    + { + store.setFieldValue("date", "date", v); + }} + className="mb-3" + /> + {store.errors.getFieldError("date", "date") && ( +
    + {store.errors.getFieldError("date", "date")} +
    + )} + store.setFieldValue("date", "verbatimEventDate", v)} + /> + {store.errors.hasSectionError("date") && ( + + {store.errors.getSectionErrors("date").join(";")} + + )}
    -}) + ); +}); diff --git a/frontend/src/pages/Encounter/LocationSectionEdit.jsx b/frontend/src/pages/Encounter/LocationSectionEdit.jsx index 3a0be81ed2..7c123b9209 100644 --- a/frontend/src/pages/Encounter/LocationSectionEdit.jsx +++ b/frontend/src/pages/Encounter/LocationSectionEdit.jsx @@ -1,69 +1,67 @@ -import React from 'react'; -import { observer } from 'mobx-react-lite'; -import SelectInput from '../../components/generalInputs/SelectInput'; +import React from "react"; +import { observer } from "mobx-react-lite"; +import SelectInput from "../../components/generalInputs/SelectInput"; import { Alert } from "react-bootstrap"; -import TextInput from '../../components/generalInputs/TextInput'; -import CoordinatesInput from '../../components/generalInputs/CoordinatesInput'; +import TextInput from "../../components/generalInputs/TextInput"; +import CoordinatesInput from "../../components/generalInputs/CoordinatesInput"; import { Suspense, lazy } from "react"; import convertToTreeData from "../../utils/converToTreeData"; -import { FormattedMessage } from 'react-intl'; - +import { FormattedMessage } from "react-intl"; const TreeSelect = lazy(() => import("antd/es/tree-select")); export const LocationSectionEdit = observer(({ store }) => { - const locationOptions = convertToTreeData(store.siteSettingsData?.locationData?.locationID, "value", "label"); + const locationOptions = convertToTreeData( + store.siteSettingsData?.locationData?.locationID, + "value", + "label", + ); - return
    - - store.setFieldValue("location", "verbatimLocality", v) - } - /> - - Loading location picker...
    }> - - store.setFieldValue("location", "locationId", val) - } - /> - - - store.setFieldValue("location", "country", v) - } - options={ - store.siteSettingsData?.country?.map((item) => { - return { - value: item, - label: item, - }; - }) || [] - } - className="mb-3" + return ( +
    + store.setFieldValue("location", "verbatimLocality", v)} + /> + + Loading location picker...
    }> + store.setFieldValue("location", "locationId", val)} /> - - {store.errors.hasSectionError("location") && ( - - {store.errors.getSectionErrors("location").join(";")} - - )} + + store.setFieldValue("location", "country", v)} + options={ + store.siteSettingsData?.country?.map((item) => { + return { + value: item, + label: item, + }; + }) || [] + } + className="mb-3" + /> + + {store.errors.hasSectionError("location") && ( + + {store.errors.getSectionErrors("location").join(";")} + + )}
    -}) + ); +}); diff --git a/frontend/src/pages/Encounter/MapDisplay.jsx b/frontend/src/pages/Encounter/MapDisplay.jsx index 0e6a781fc4..f59ba5162d 100644 --- a/frontend/src/pages/Encounter/MapDisplay.jsx +++ b/frontend/src/pages/Encounter/MapDisplay.jsx @@ -1,3 +1,4 @@ +/* eslint-disable no-unused-vars */ import React, { useEffect, useRef } from "react"; import { observer } from "mobx-react-lite"; import { Loader } from "@googlemaps/js-api-loader"; @@ -26,8 +27,12 @@ export const MapDisplay = observer( const el = mapElRef.current; if (!el || !el.isConnected) return; - const hasCoords = Number.isFinite(Number(store.lat)) && Number.isFinite(Number(store.lon)); - const center = hasCoords ? { lat: Number(store.lat), lng: Number(store.lon) } : fallbackCenter; + const hasCoords = + Number.isFinite(Number(store.lat)) && + Number.isFinite(Number(store.lon)); + const center = hasCoords + ? { lat: Number(store.lat), lng: Number(store.lon) } + : fallbackCenter; map = new window.google.maps.Map(el, { center, @@ -55,7 +60,6 @@ export const MapDisplay = observer( store.lon, ]); - return (
    { - - const { locale } = useContext(LocaleContext); - const samplingProtocols = store.siteSettingsData?.samplingProtocol || []; - const samplingProtocolOptions = samplingProtocols.map(protocol => ({ - label: protocol.label[locale] || protocol.label['en'], - value: protocol.value - })); - return
    - {store.showMeasurements && store.measurementTypes && store.measurementTypes.length > 0 && ( - <> - {store.measurementTypes.map((type, index) => { - const unitLabel = store.measurementUnits?.[index] || 'Unit'; - const cur = store.getMeasurement(type); - return ( -
    -
    {type}
    - { - store.errors.setFieldError("measurement", type, null); - if (value === '') { - store.errors.setFieldError("measurement", type, "Value cannot be empty"); - } - store.setMeasurementValue(type, value); - }} - /> - { - store.errors.setFieldError("measurement", type, null); - if (!samplingProtocol || samplingProtocol === '') { - store.errors.setFieldError("measurement", type, "Value cannot be empty"); - } - store.setMeasurementSamplingProtocol(type, samplingProtocol); - }} - /> - - {store.errors.getFieldError("measurement", type) && ( -
    - {store.errors.getFieldError("measurement", type) || ""} -
    - )} -
    - ); - })} - + const { locale } = useContext(LocaleContext); + const samplingProtocols = store.siteSettingsData?.samplingProtocol || []; + const samplingProtocolOptions = samplingProtocols.map((protocol) => ({ + label: protocol.label[locale] || protocol.label["en"], + value: protocol.value, + })); + return ( +
    + {store.showMeasurements && + store.measurementTypes && + store.measurementTypes.length > 0 && ( + <> + {store.measurementTypes.map((type, index) => { + const unitLabel = store.measurementUnits?.[index] || "Unit"; + const cur = store.getMeasurement(type); + return ( +
    +
    {type}
    + { + store.errors.setFieldError("measurement", type, null); + if (value === "") { + store.errors.setFieldError( + "measurement", + type, + "Value cannot be empty", + ); + } + store.setMeasurementValue(type, value); + }} + /> + { + store.errors.setFieldError("measurement", type, null); + if (!samplingProtocol || samplingProtocol === "") { + store.errors.setFieldError( + "measurement", + type, + "Value cannot be empty", + ); + } + store.setMeasurementSamplingProtocol( + type, + samplingProtocol, + ); + }} + /> + + {store.errors.getFieldError("measurement", type) && ( +
    + {store.errors.getFieldError("measurement", type) || ""} +
    + )} +
    + ); + })} + )}
    -}) + ); +}); diff --git a/frontend/src/pages/Encounter/MeasurementsReview.jsx b/frontend/src/pages/Encounter/MeasurementsReview.jsx index 42ff67b413..298a7622e2 100644 --- a/frontend/src/pages/Encounter/MeasurementsReview.jsx +++ b/frontend/src/pages/Encounter/MeasurementsReview.jsx @@ -1,23 +1,31 @@ -import React from 'react'; -import { observer } from 'mobx-react-lite'; +import React from "react"; +import { observer } from "mobx-react-lite"; -export const MeasurementsReview = observer(({ store ={} }) => { - return
    - {store.showMeasurements && ( - <> - {store.measurementTypes?.map((type, index) => { - const measurement = store.encounterData?.measurements?.find(m => m.type === type); - if (!measurement) return
    -
    {type}
    -

    {`${" "}`}

    -
    ; - return
    -
    {measurement.type}
    -

    {`${measurement.value}${" "}${measurement.units}(${measurement.samplingProtocol})`}

    -
    - }) - } - - )} +export const MeasurementsReview = observer(({ store = {} }) => { + return ( +
    + {store.showMeasurements && ( + <> + {store.measurementTypes?.map((type, index) => { + const measurement = store.encounterData?.measurements?.find( + (m) => m.type === type, + ); + if (!measurement) + return ( +
    +
    {type}
    +

    {`${" "}`}

    +
    + ); + return ( +
    +
    {measurement.type}
    +

    {`${measurement.value}${" "}${measurement.units}(${measurement.samplingProtocol})`}

    +
    + ); + })} + + )}
    -}) + ); +}); diff --git a/frontend/src/pages/Encounter/constants.js b/frontend/src/pages/Encounter/constants.js index 7758bae89b..4690983abb 100644 --- a/frontend/src/pages/Encounter/constants.js +++ b/frontend/src/pages/Encounter/constants.js @@ -1,4 +1,3 @@ - const SECTION_FIELD_PATHS = { date: ["date", "verbatimEventDate"], identify: [ @@ -9,12 +8,7 @@ const SECTION_FIELD_PATHS = { "sightingId", "individualId", ], - metadata: [ - "id", - "submitterID", - "state", - "observationComments", - ], + metadata: ["id", "submitterID", "state", "observationComments"], location: [ "verbatimLocality", "locationId", @@ -37,7 +31,7 @@ const SECTION_FIELD_PATHS = { const LOCAL_FIELD_ERRORS = { date: { - date: "invalid date" + date: "invalid date", }, location: { latitude: "please enter invalid latitude and longitude", @@ -45,7 +39,7 @@ const LOCAL_FIELD_ERRORS = { }, measurements: { measurements: "please enter valid values", - } + }, }; -export { SECTION_FIELD_PATHS, LOCAL_FIELD_ERRORS }; \ No newline at end of file +export { SECTION_FIELD_PATHS, LOCAL_FIELD_ERRORS }; diff --git a/frontend/src/pages/Encounter/stores/ModalStore.js b/frontend/src/pages/Encounter/stores/ModalStore.js index d672d5696e..646cafea5c 100644 --- a/frontend/src/pages/Encounter/stores/ModalStore.js +++ b/frontend/src/pages/Encounter/stores/ModalStore.js @@ -2,7 +2,7 @@ import { makeAutoObservable } from "mobx"; class ModalStore { - encounterStore; + encounterStore; _openContactInfoModal = false; _openEncounterHistoryModal = false; @@ -59,4 +59,4 @@ class ModalStore { } } -export default ModalStore; \ No newline at end of file +export default ModalStore; diff --git a/frontend/src/pages/Encounter/stores/index.js b/frontend/src/pages/Encounter/stores/index.js index 65c5685f85..c9c3a3eb3c 100644 --- a/frontend/src/pages/Encounter/stores/index.js +++ b/frontend/src/pages/Encounter/stores/index.js @@ -1,3 +1,3 @@ // stores/index.js -export { default as EncounterStore } from './EncounterStore'; -export { default as ModalStore } from './ModalStore'; \ No newline at end of file +export { default as EncounterStore } from "./EncounterStore"; +export { default as ModalStore } from "./ModalStore"; diff --git a/frontend/src/pages/ReportsAndManagamentPages/ImageSection.jsx b/frontend/src/pages/ReportsAndManagamentPages/ImageSection.jsx index 2521425cfb..174df311ea 100644 --- a/frontend/src/pages/ReportsAndManagamentPages/ImageSection.jsx +++ b/frontend/src/pages/ReportsAndManagamentPages/ImageSection.jsx @@ -14,7 +14,6 @@ import useGetSiteSettings from "../../models/useGetSiteSettings"; import { observer } from "mobx-react-lite"; import { Alert } from "react-bootstrap"; import EXIF from "exif-js"; -import { defaultMaxMediaSize } from "../../constants/photoUpload.js"; export const ImageSection = observer(({ store }) => { const [files, setFiles] = useState([]); @@ -24,7 +23,7 @@ export const ImageSection = observer(({ store }) => { const [previewData, setPreviewData] = useState([]); const fileInputRef = useRef(null); const { data } = useGetSiteSettings(); - const maxSize = data?.maximumMediaSizeMegabytes || defaultMaxMediaSize; + const maxSize = data?.maximumMediaSizeMegabytes || 40; const theme = useContext(ThemeContext); const originalBorder = `1px dashed ${theme.primaryColors.primary500}`; const updatedBorder = `2px dashed ${theme.primaryColors.primary500}`; diff --git a/frontend/src/pages/ReportsAndManagamentPages/ReportEncounterStore.js b/frontend/src/pages/ReportsAndManagamentPages/ReportEncounterStore.js index aeb97f37c6..25349e6d88 100644 --- a/frontend/src/pages/ReportsAndManagamentPages/ReportEncounterStore.js +++ b/frontend/src/pages/ReportsAndManagamentPages/ReportEncounterStore.js @@ -53,16 +53,13 @@ export class ReportEncounterStore { submitter: { name: "", email: "", - emailError: false, }, photographer: { name: "", email: "", - emailError: false, }, additionalEmails: "", error: false, - additionalEmailsError: false, }; this._success = false; this._finished = false; @@ -221,18 +218,6 @@ export class ReportEncounterStore { this._followUpSection.value = value; } - setSubmitterEmailError(error) { - this._followUpSection.submitter.emailError = error; - } - - setPhotographerEmailError(error) { - this._followUpSection.photographer.emailError = error; - } - - setAdditionalEmailsError(error) { - this._followUpSection.additionalEmailsError = error; - } - setCommentsSectionValue(value) { this._additionalCommentsSection.value = value; } @@ -279,38 +264,26 @@ export class ReportEncounterStore { validateEmails() { const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - let isValid = true; - - this.setSubmitterEmailError(false); - this.setPhotographerEmailError(false); - this.setAdditionalEmailsError(false); if (this._followUpSection.submitter.email) { - if (!emailPattern.test(this._followUpSection.submitter.email)) { - this.setSubmitterEmailError(true); - isValid = false; - } + if (!emailPattern.test(this._followUpSection.submitter.email)) + return false; } if (this._followUpSection.photographer.email) { - if (!emailPattern.test(this._followUpSection.photographer.email)) { - this.setPhotographerEmailError(true); - isValid = false; - } + if (!emailPattern.test(this._followUpSection.photographer.email)) + return false; } if (this._followUpSection.additionalEmails) { - const allEmailsValid = this._followUpSection.additionalEmails + return this._followUpSection.additionalEmails .split(",") - .every((email) => emailPattern.test(email.trim())); - - if (!allEmailsValid) { - this.setAdditionalEmailsError(true); - isValid = false; - } + .every((email) => { + return emailPattern.test(email.trim()); + }); } - return isValid; + return true; } validateFields() { diff --git a/frontend/src/utils/treeSelectionFunction.js b/frontend/src/utils/treeSelectionFunction.js index 3425514cd9..c0c1d8a229 100644 --- a/frontend/src/utils/treeSelectionFunction.js +++ b/frontend/src/utils/treeSelectionFunction.js @@ -23,7 +23,7 @@ export function expandIds(locationOptions, values = []) { (values || []).forEach((val) => { if (!val) return; set.add(val); - const node = findNodeByValue(locationOptions, val); + const node = findNodeByValue(locationOptions, val); if (node) { getAllDescendantValues(node).forEach((cid) => { if (cid != null) set.add(cid); @@ -31,4 +31,4 @@ export function expandIds(locationOptions, values = []) { } }); return Array.from(set); -} \ No newline at end of file +} diff --git a/src/main/java/org/ecocean/CommonConfiguration.java b/src/main/java/org/ecocean/CommonConfiguration.java index 95f01f2db6..fdcd0430f5 100644 --- a/src/main/java/org/ecocean/CommonConfiguration.java +++ b/src/main/java/org/ecocean/CommonConfiguration.java @@ -432,9 +432,8 @@ public static String getHTMLDescription(String context) { return getProperty("htmlDescription", context).trim(); } - // Refer to PhotoUploadDevNotes.md for information/where else to change public static int getMaxMediaSizeInMegabytes(String context) { - int maxSize = 3; + int maxSize = 10; try { String sMaxSize = getProperty("maxMediaSize", context); @@ -449,8 +448,6 @@ public static int getMaxMediaSizeInMegabytes(String context) { } return maxSize; } - - // Refer to PhotoUploadDevNotes.md for information/where else to change public static int getMaxMediaCountEncounter(String context) { int maxCount = 200; diff --git a/src/main/java/org/ecocean/servlet/EncounterSetGenusSpecies.java b/src/main/java/org/ecocean/servlet/EncounterSetGenusSpecies.java index 3761a6def4..8dcb6b009c 100644 --- a/src/main/java/org/ecocean/servlet/EncounterSetGenusSpecies.java +++ b/src/main/java/org/ecocean/servlet/EncounterSetGenusSpecies.java @@ -59,6 +59,11 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) myShark.setGenus(genus); specificEpithet = tokenizer.nextToken(); + //check for subspecies + if(tokenizer.hasMoreTokens()) { + specificEpithet += " " + tokenizer.nextToken(); + } + myShark.setSpecificEpithet(specificEpithet.replaceAll(",", "").replaceAll("_", " ")); myShark.addComments("

    " + request.getRemoteUser() + " on " + diff --git a/src/main/resources/bundles/commonConfiguration.properties b/src/main/resources/bundles/commonConfiguration.properties index dcdc6a69d2..df23b2de97 100755 --- a/src/main/resources/bundles/commonConfiguration.properties +++ b/src/main/resources/bundles/commonConfiguration.properties @@ -305,7 +305,7 @@ biologicalMeasurementSamplingProtocols2 = No lipids extracted, uncorrected #Maximum uploadable media size in megabytes (MB) #This value is used for encounter images and videos as well as for file associations added to a MarkedIndividual. -maxMediaSize = 3 +maxMediaSize = 40 #Maximum number of media per encounter maximumMediaCountEncounter = 200