diff --git a/locale/en.json b/locale/en.json index d21b17c66..c453f4018 100644 --- a/locale/en.json +++ b/locale/en.json @@ -11,6 +11,7 @@ "SOCIAL_GROUP_SUBHEADER": "Social group created on {dateCreated}", "ALL_SOCIAL_GROUPS": "All social groups", "BULK_IMPORT": "Bulk import", + "DATA_PAGE" : "Data page", "LIME": "Lime", "LABEL": "Label", "TYPE": "Type", @@ -200,6 +201,7 @@ "SEARCH_ITIS_SPECIES": "Search ITIS species", "SEARCH_INDIVIDUALS_INSTRUCTION": "Search for an individual by name or guid", "SEARCH_SIGHTINGS_INSTRUCTION": "Search for a sighting by location, owner or guid", + "SEARCH_ANIMALS_INSTRUCTION": "Search for an animal by location or owner", "SEARCH_USER_INSTRUCTION": "Search for a user by name or email", "PRAIRIE": "Prairie", "EDIT_USER_METADATA": "Edit user metadata", @@ -547,6 +549,8 @@ "SPECIAL_INPUTS": "SPECIAL FIELDS", "SIGHTING_SEARCH_RESULT_PRIMARY_TEXT": "{date} sighting in {region}", "SIGHTING_SEARCH_RESULT_SECONDARY_TEXT": "Reported by {name} on {date}", + "ANIMAL_SEARCH_RESULT_PRIMARY_TEXT": "{date} sighting in {region}", + "ANIMAL_SEARCH_RESULT_SECONDARY_TEXT": "Reported by {name} on {date}", "SELECTED_QUERY_ANNOTATION": "Selected query annotation", "IDENTIFICATION_FINISHED_TIME": "Identification finished on {time}.", "SELECTED_MATCH_CANDIDATE": "Selected match candidate", @@ -637,6 +641,7 @@ "SPECIES:": "Species:", "EXPLORE_SIGHTINGS_CAPITALIZED": "Explore Sightings", "EXPLORE_SIGHTINGS": "Explore sightings", + "EXPLORE_ANIMALS": "Explore animals", "ATTRIBUTES": "Attributes", "RELATIONSHIPS": "Relationships", "RELATIONSHIPS_DESCRIPTION": "Known or observed social and familial relationships.", @@ -986,6 +991,7 @@ "COMPONENT_COMMIT_HASH": "{component} commit hash: ", "INDIVIDUAL_SEARCH_NO_RESULTS": "Your search \"{searchTerm}\" did not match any individuals.", "SIGHTING_SEARCH_NO_RESULTS": "Your search \"{searchTerm}\" did not match any sightings.", + "ENCOUNTER_SEARCH_NO_RESULTS": "Your search \"{searchTerm}\" did not match any sightings.", "POTENTIAL_COLLABORATOR_SEARCH_NO_RESULTS": "Your search \"{searchTerm}\" did not match any potential collaborators.", "SEARCH_SERVER_ERROR": "A server error occurred while attempting to search.", "CONFIGURATION_SITE_NAME_LABEL": "Site name", @@ -1139,8 +1145,8 @@ "ROLE_GUID_MISSING": "Role is missing ID", "UNFINISHED_OPTIONS": "Options must have valid values and labels and unique values", "PROGRESS_STATISTICS_UNKNOWN_PROGRESS": "unknown progress", - "PROGRESS_STATISTICS_UNKNOWN_ETA_&_UNKNOWN_QUEUE": "Unknown time remaining. Queued behind an unknown number of jobs.", - "PROGRESS_STATISTICS_UNKNOWN_ETA_&_QUEUE": "Unknown time remaining. Queued behind {ahead, number} {ahead, plural, one {job} other {jobs}}.", + "PROGRESS_STATISTICS_UNKNOWN_ETA_&_UNKNOWN_QUEUE": "Estimated time to complete failed to calculate. If processing does not complete within a day, re-run the job. Queued behind an unknown number of jobs.", + "PROGRESS_STATISTICS_UNKNOWN_ETA_&_QUEUE": "Estimated time to complete failed to calculate. If processing does not complete within a day, re-run the job. Queued behind {ahead, number} {ahead, plural, one {job} other {jobs}}.", "PROGRESS_STATISTICS_WRAPPING_ETA_&_UNKNOWN_QUEUE": "Wrapping up... Queued behind an unknown number of jobs.", "PROGRESS_STATISTICS_WRAPPING_ETA_&_QUEUE": "Wrapping up... Queued behind {ahead, number} {ahead, plural, one {job} other {jobs}}.", "PROGRESS_STATISTICS_ETA_&_UNKNOWN_QUEUE": "{timeRemaining} left. Queued behind an unknown number of jobs.", @@ -1271,5 +1277,18 @@ "CONFIRM_NO_MATCH" : "Confirm no match", "NUMBER_OF_INDIVIDUALS" : "Number of individuals", "NUMBER_OF_ENCOUNTERS" : "Number of animals", - "ASSIGN" : "Assign" + "NUMBER_OF_ANNOTATIONS" : "Number of annotations", + "ASSIGN" : "Assign", + "STATE" : "State", + "ENCOUNTER_TIME" : "Time of encounter", + "LATITUDE" : "Latitude", + "LONGTITUDE" : "Longtitude", + "MY_PENDING_SIGHTINGS" : "My pending sightings", + "MY_SIGHTINGS" : "My sightings", + "MY_UNAPPROVED_SIGHTINGS" : "My unapproved sightings", + "TotalAccount" : "Total Account: {totalAccount}", + "MANAGE_REGIONS" : "Manage Regions", + "EXPORT_RESULT" : "Export result", + "EXPORT_ACCESS_RESTRICTED_WARNING" : "No results meet export criteria. Consider adjusting your search or requesting export collaborations.", + "OK" : "Ok" } diff --git a/src/AuthenticatedSwitch.jsx b/src/AuthenticatedSwitch.jsx index ec70c46fc..eff39216b 100644 --- a/src/AuthenticatedSwitch.jsx +++ b/src/AuthenticatedSwitch.jsx @@ -7,7 +7,6 @@ import AuthenticatedAppHeader from './components/AuthenticatedAppHeader'; import SaveCustomField from './pages/fieldManagement/settings/saveField/SaveField'; import GeneralSettings from './pages/generalSettings/GeneralSettings'; import SiteStatus from './pages/siteStatus/SiteStatus'; -import SplashSettings from './pages/splashSettings/SplashSettings'; import FieldManagement from './pages/fieldManagement/FieldManagement'; import UserManagement from './pages/userManagement/UserManagement'; import AdminActions from './pages/adminActions/AdminActions'; @@ -36,19 +35,21 @@ import FourOhFour from './pages/fourohfour/FourOhFour'; import useSiteSettings from './models/site/useSiteSettings'; import SearchIndividuals from './pages/individual/SearchIndividuals'; import SearchSightings from './pages/sighting/SearchSightings'; +import SearchAnimals from './pages/sighting/encounters/SearchAnimals'; import SiteSetup from './pages/setup/SiteSetup'; import MatchSighting from './pages/match/MatchSighting'; import AuditLog from './pages/devTools/AuditLog'; import Welcome from './pages/auth/Welcome'; import EmailVerified from './pages/auth/EmailVerified'; import Home from './pages/home/Home'; -import Preferences from './pages/preferences/Preferences'; import ResendVerificationEmail from './pages/auth/ResendVerificationEmail'; import Footer from './components/Footer'; import { defaultCrossfadeDuration } from './constants/defaults'; import Requests from './pages/setup/Requests'; import SpeciesManagement from './pages/fieldManagement/SpeciesManagement'; +import RegionManagement from './pages/fieldManagement/RegionManagement'; import ChangeLog from './pages/changeLog/ChangeLog'; +import DataPage from './pages/dataPage/DataPage'; export default function AuthenticatedSwitch({ emailNeedsVerification, @@ -100,9 +101,6 @@ export default function AuthenticatedSwitch({ ) : ( - - - @@ -118,6 +116,9 @@ export default function AuthenticatedSwitch({ + + + @@ -130,9 +131,6 @@ export default function AuthenticatedSwitch({ - - - @@ -166,6 +164,9 @@ export default function AuthenticatedSwitch({ + + + @@ -184,6 +185,9 @@ export default function AuthenticatedSwitch({ + + + @@ -214,9 +218,12 @@ export default function AuthenticatedSwitch({ - + + + + diff --git a/src/components/AuthenticatedAppHeader/ActionsPane.jsx b/src/components/AuthenticatedAppHeader/ActionsPane.jsx index 518b73238..cb084c4a7 100644 --- a/src/components/AuthenticatedAppHeader/ActionsPane.jsx +++ b/src/components/AuthenticatedAppHeader/ActionsPane.jsx @@ -10,6 +10,7 @@ import Divider from '@material-ui/core/Divider'; import PublicIcon from '@material-ui/icons/SupervisedUserCircle'; import ControlPanelIcon from '@material-ui/icons/PermDataSetting'; import BulkImportIcon from '@material-ui/icons/PostAdd'; +import InsertChartOutlinedOutlinedIcon from '@material-ui/icons/InsertChartOutlined'; import LogoutIcon from '@material-ui/icons/ExitToApp'; import Link from '../Link'; @@ -17,6 +18,12 @@ import Text from '../Text'; import defaultProfilePhoto from '../../assets/defaultProfile.jpg'; const actions = [ + { + id: 'data-page', + href: '/data-page', + messageId: 'DATA_PAGE', + icon: InsertChartOutlinedOutlinedIcon, + }, { id: 'bulk-import', href: '/bulk-import', @@ -87,7 +94,7 @@ export default function NotificationsPane({ onClose={closePopover} > - + + {closePopover => ( + <> + {showDivider && } + {resultsCurrent && noResults && ( + + )} + {resultsCurrent && error && ( + + )} + + {mappableSearchResults.map(encounter => { + const encounterGuid = encounter?.guid; + const sightingGuid = encounter?.sighting_guid; + const ownerName = get( + encounter, + ['owner', 'full_name'], + 'Unknown User', + ); + const regionLabel = encounter?.locationId_value; + const createdDate = formatDate( + encounter?.created, + true, + ); + const encounterDate = formatSpecifiedTime( + encounter?.time, + encounter?.timeSpecificity, + ); + const avatarLetter = ownerName[0].toUpperCase(); + + return ( + + } + secondaryText={ + + } + /> + ); + })} + + + )} + + ); +} diff --git a/src/components/AuthenticatedAppHeader/index.js b/src/components/AuthenticatedAppHeader/index.js index 0aed4a728..c9775ac03 100644 --- a/src/components/AuthenticatedAppHeader/index.js +++ b/src/components/AuthenticatedAppHeader/index.js @@ -24,6 +24,7 @@ import NotificationsPane from './NotificationsPane'; import ActionsPane from './ActionsPane'; import IndividualsButton from './IndividualsButton'; import SightingsButton from './SightingsButton'; +import AnimalsButton from './AnimalsButton'; import queryKeys from '../../constants/queryKeys'; export default function AppHeader() { @@ -99,6 +100,7 @@ export default function AppHeader() { + )} diff --git a/src/components/EditUserMetadata.jsx b/src/components/EditUserMetadata.jsx index 05b52190d..a9baf77b8 100644 --- a/src/components/EditUserMetadata.jsx +++ b/src/components/EditUserMetadata.jsx @@ -12,6 +12,21 @@ import InputRow from './fields/edit/InputRow'; import Button from './Button'; import PasswordVerificationAlert from './PasswordVerificationAlert'; import StandardDialog from './StandardDialog'; +import Typography from '@material-ui/core/Typography'; +import { useTheme } from '@material-ui/core/styles'; +import EntityHeader from './EntityHeader'; +import BigAvatar from './profilePhotos/BigAvatar'; +import RequestCollaborationButton from './RequestCollaborationButton'; +import Text from './Text'; +import Chip from '@material-ui/core/Chip'; +import { MailOutline } from '@material-ui/icons'; +import UserProfileMetadataWrap from './UserProfileMetadataWrap'; +import AccountCircleOutlinedIcon from '@material-ui/icons/AccountCircleOutlined'; +import ForumOutlinedIcon from '@material-ui/icons/ForumOutlined'; +import AccountBalanceOutlined from '@material-ui/icons/AccountBalanceOutlined'; +import PlaceOutlined from '@material-ui/icons/PlaceOutlined'; + + function getInitialFormValues(schema) { return schema.reduce((memo, field) => { @@ -31,6 +46,14 @@ const twitterMetadataKey = twitterSchema?.userMetadataKey; export default function EditUserMetadata({ open, userId, + imageGuid, + imageSrc, + name, + refreshUserData, + userDataLoading, + communityUsername, + dateCreated, + highestRoleLabelId, metadata, onClose, }) { @@ -41,6 +64,8 @@ export default function EditUserMetadata({ clearError, } = useReplaceUserProperties(); + const theme = useTheme(); + const [fieldValues, setFieldValues] = useState({}); const [passwordRequired, setPasswordRequired] = useState(false); const [password, setPassword] = useState(''); @@ -49,23 +74,118 @@ export default function EditUserMetadata({ setFieldValues(getInitialFormValues(metadata)); }, [metadata]); + return ( + + + } + renderOptions={ + + + + } + > +
+
+ {communityUsername && <> + + {`@${communityUsername}`} + +
+ } + + +
+ +
+ + + + {metadata.map(field => { if (!field.editable) return null; if (!field.editComponent) return null; // temporary stopgap const value = get(fieldValues, field.name, ''); const fieldProps = field.editComponentProps || {}; + const labelId = get(field, 'labelId'); return ( +
+ {labelId === 'FULL_NAME' && + + + } + {labelId === 'PROFILE_LABEL_EMAIL' && + + + } + {labelId === 'PROFILE_LABEL_FORUM_ID' && + + + } + {labelId === 'PROFILE_LABEL_AFFILIATION' && + + + } + {labelId === 'PROFILE_LABEL_LOCATION' && + + + } + + <> + +
); })} diff --git a/src/components/EntityHeader.jsx b/src/components/EntityHeader.jsx index a87d0f206..831e93569 100644 --- a/src/components/EntityHeader.jsx +++ b/src/components/EntityHeader.jsx @@ -13,6 +13,7 @@ export default function EntityHeader({ renderAvatar, renderTabs, codexID, + noDivider=false, }) { const theme = useTheme(); return ( @@ -81,7 +82,7 @@ export default function EntityHeader({ - + {!noDivider &&} ); } \ No newline at end of file diff --git a/src/components/PreferenceModal.jsx b/src/components/PreferenceModal.jsx new file mode 100644 index 000000000..bd55fd33e --- /dev/null +++ b/src/components/PreferenceModal.jsx @@ -0,0 +1,214 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { get, has } from 'lodash-es'; + +import useDocumentTitle from '../hooks/useDocumentTitle'; +import MainColumn from '../components/MainColumn'; +import UserDeleteDialog from '../components/dialogs/UserDeleteDialog'; +import Button from '../components/Button'; +import InputRow from '../components/fields/edit/InputRow'; +import Text from '../components/Text'; +import ErrorDialog from '../components/dialogs/ErrorDialog'; +import useGetMe from '../models/users/useGetMe'; +import { useReplaceUserProperty } from '../models/users/usePatchUser'; +import { useNotificationSettingsSchemas } from '../pages/preferences/useUserSettingsSchemas'; +import { deriveNotificationPreferences } from '../pages/preferences/deriveNotificationPreferences'; +import StandardDialog from '../components/StandardDialog'; +import DialogContent from '@material-ui/core/DialogContent'; +import MailOutline from '@material-ui/icons/MailOutline'; +import EditOutline from '@material-ui/icons/EditOutlined'; +import GroupAdd from '@material-ui/icons/GroupAdd'; +import CallMerge from '@material-ui/icons/CallMerge'; +import UserProfileMetadataWrap from '../components/UserProfileMetadataWrap'; + + + +function getInitialFormValues(schemas, data) { + return schemas.reduce((memo, field) => { + const valueKey = get(field, 'name'); + const defaultValue = get(field, 'defaultValue'); + memo[valueKey] = get(data, valueKey, defaultValue); + return memo; + }, {}); +} + +export default function Preferences({open, onClose}) { + useDocumentTitle('PREFERENCES'); + + const { data } = useGetMe(); + + const [deactivating, setDeactivating] = useState(false); + + const { + mutate: replaceUserProperty, + loading, + error, + clearError, + } = useReplaceUserProperty(); + const schemas = useNotificationSettingsSchemas(); + + const [formValues, setFormValues] = useState({}); + useEffect(() => { + const initialValues = getInitialFormValues(schemas, data); + + setFormValues(prevFormValues => ({ + ...initialValues, + ...prevFormValues, + })); + }, [schemas, data]); + + const backendValues = useMemo( + () => getInitialFormValues(schemas, data), + [schemas, data], + ); + + return ( + + + + { + clearError(); + }} + errorMessage={error} + /> + {deactivating && ( + setDeactivating(false)} + userData={data} + deactivatingSelf + /> + )} + + + + {schemas.map(notificationField => { + const fieldKey = get(notificationField, 'name'); + const fieldValue = get(formValues, fieldKey); + const backendValue = get(backendValues, fieldKey); + const valueHasChanged = fieldValue !== backendValue; + const labelId = get(notificationField, 'labelId'); + + return ( +
+ {labelId === 'ALL_NOTIFICATION_EMAILS' && + + + } + {labelId === 'COLLABORATION_REQUESTS' && + + + } + {labelId === 'COLLABORATION_EDIT_REQUESTS' && + + + } + {labelId === 'MERGE_OF_INDIVIDUAL' && + + + } + + <> + +
+ + { + setFormValues({ + ...formValues, + [fieldKey]: !fieldValue, + }); + }} + minimalLabels + /> + + {valueHasChanged && ( +
+
+ )} +
+
+ +
+ ); + })} + + + + + +
+ + +
+ + + + + {children}
- - setEditingProfile(true) // ? - } - metadata={metadata} - /> + +
+ {/* } projects={[ @@ -125,60 +208,26 @@ export default function UserProfile({ ]} /> */} - - - - {!someoneElse && ( - - - - )} - {someoneElse && viewerIsUserManager && ( - - - - )} + + + + {!someoneElse && ( + + + + )} + {someoneElse && viewerIsUserManager && ( + + + + )} + +
diff --git a/src/components/UserProfileMetaDataDisplay.jsx b/src/components/UserProfileMetaDataDisplay.jsx new file mode 100644 index 000000000..08894874d --- /dev/null +++ b/src/components/UserProfileMetaDataDisplay.jsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { + MailOutline, + PlaceOutlined, + AccountBalanceOutlined, +} from '@material-ui/icons'; +import UserProfileMetadataWrap from './UserProfileMetadataWrap'; + +export default function UserProfileMetaDataDisplay({ + email, + location, + affiliation, +}) { + return ( +
+ + + + + + + + + + + +
+ ); +} diff --git a/src/components/UserProfileMetadataWrap.jsx b/src/components/UserProfileMetadataWrap.jsx new file mode 100644 index 000000000..a7617c071 --- /dev/null +++ b/src/components/UserProfileMetadataWrap.jsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { useTheme } from '@material-ui/core/styles'; +import Text from './Text'; + +export default function UserProfileMetadataWrap({ + id, + value, + children, +}) { + const theme = useTheme(); + return ( +
+
+ {children} +
+
+ + +
+
+ ); +} diff --git a/src/components/cards/SightingsCard.jsx b/src/components/cards/SightingsCard.jsx index 9db060131..52984fcfd 100644 --- a/src/components/cards/SightingsCard.jsx +++ b/src/components/cards/SightingsCard.jsx @@ -24,6 +24,8 @@ export default function SightingsCard({ linkPath = 'sightings', noSightingsMsg = 'NO_SIGHTINGS', loading, + searchParams, + setSearchParams, }) { const [showMapView, setShowMapView] = useState(false); const theme = useTheme(); @@ -33,23 +35,20 @@ export default function SightingsCard({ const mapModeClicked = () => setShowMapView(true); const listModeClicked = () => setShowMapView(false); - const sightingsWithLocationData = useMemo( - () => { - // hotfix // - if (!sightings) return []; - // hotfix // + const sightingsWithLocationData = useMemo(() => { + // hotfix // + if (!sightings) return []; + // hotfix // - return sightings.map(sighting => ({ - ...sighting, - formattedLocation: formatLocationFromSighting( - sighting, - regionOptions, - intl, - ), - })); - }, - [get(sightings, 'length')], - ); + return sightings.map(sighting => ({ + ...sighting, + formattedLocation: formatLocationFromSighting( + sighting, + regionOptions, + intl, + ), + })); + }, [get(sightings, 'length')]); const allColumns = [ { @@ -81,12 +80,29 @@ export default function SightingsCard({ name: 'locationId_value', labelId: 'LOCATION', }, + { + reference: 'match_state', + name: 'match_state', + labelId: 'STATE', + }, + { + reference: 'numberAnnotations', + name: 'numberAnnotations', + labelId: 'NUMBER_OF_ANNOTATIONS', + }, + { + reference: 'numberEncounters', + name: 'numberEncounters', + labelId: 'NUMBER_OF_ENCOUNTERS', + }, { reference: 'actions', name: 'guid', labelId: 'ACTIONS', options: { - customBodyRender: value => ( + customBodyRender: ( + value, // eslint-disable-line + ) => (
onDelete(value)} + // onClick={() => { + // setDeleteDialogOpen(true); + // }} /> )}
@@ -152,7 +171,10 @@ export default function SightingsCard({ data={sightings} loading={loading} noResultsTextId={noSightingsMsg} - tableContainerStyles={{ maxHeight: 400 }} + tableContainerStyles={{ maxHeight: 800 }} + searchParams={searchParams} + setSearchParams={setSearchParams} + dataCount /> )} {!noSightings && showMapView && ( diff --git a/src/components/dataDisplays/DataDisplay.jsx b/src/components/dataDisplays/DataDisplay.jsx index 4f539d33a..c89a05d45 100644 --- a/src/components/dataDisplays/DataDisplay.jsx +++ b/src/components/dataDisplays/DataDisplay.jsx @@ -24,15 +24,19 @@ import FilterList from '@material-ui/icons/FilterList'; import CloudDownload from '@material-ui/icons/CloudDownload'; import axios from 'axios'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogActions from '@material-ui/core/DialogActions'; import BaoDetective from '../svg/BaoDetective'; import FilterBar from '../FilterBar'; import Text from '../Text'; import CollapsibleRow from './CollapsibleRow'; import sendCsv, { downloadFileFromBackend } from './sendCsv'; -import useGetMe from '../../models/users/useGetMe'; import buildSightingsQuery from './buildSightingsQuery'; import buildIndividualsQuery from './buildIndividualsQuery'; +import buildEncountersQuery from './buildEncountersQuery'; +import StandardDialog from '../StandardDialog'; +import Button from '../Button'; function getCellAlignment(cellIndex, columnDefinition) { if (columnDefinition.align) return columnDefinition.align; @@ -77,11 +81,6 @@ export default function DataDisplay({ const theme = useTheme(); const themeColor = theme.palette.primary.main; - const { data: currentUserData } = useGetMe(); - - const isAdmin = get(currentUserData, 'is_admin', false); - const isExporter = get(currentUserData, 'is_exporter', false); - const initialColumnNames = columns .filter(c => get(c, 'options.display', true)) .map(c => c.name); @@ -96,6 +95,7 @@ export default function DataDisplay({ useState(null); const [anchorEl, setAnchorEl] = useState(null); const filterPopperOpen = Boolean(anchorEl); + const [dialogOpen, setDialogOpen] = useState(false); const selectedRow = selectionControlled ? selectedRowFromProps @@ -149,6 +149,33 @@ export default function DataDisplay({ return (
+ setDialogOpen(false)} + fullWidth + maxWidth="md" + > + + + + +
+ } + > + {Array.isArray(node.locationID) + ? node.locationID.map((node) => renderItem(node, searchText)) + : null} + + ) + } + + + return ( +
+ } + defaultExpandIcon={} + > + {(_.isNil(showData) || !_.isArray(showData) || _.isEmpty(showData)) + ? <> + : showData.map(node => renderItem(node))} + +
+ ); +}; + +export default TreeViewComponent; \ No newline at end of file diff --git a/src/components/filterFields/useBuildFilter.js b/src/components/filterFields/useBuildFilter.js index 245e0fb29..09ed5fa2a 100644 --- a/src/components/filterFields/useBuildFilter.js +++ b/src/components/filterFields/useBuildFilter.js @@ -18,7 +18,11 @@ export default function useBuildFilter(fields, component) { const { booleanChoices } = useOptions(); let queryTerm; let queryTerms; - if (component === 'sightings' || component === 'individuals') { + if ( + component === 'sightings' || + component === 'individuals' || + !component + ) { queryTerm = 'customFields'; queryTerms = 'customFields'; } else if (component === 'encounters') { diff --git a/src/components/profilePhotos/BigAvatar.jsx b/src/components/profilePhotos/BigAvatar.jsx index 72c61c3c2..551b4aec8 100644 --- a/src/components/profilePhotos/BigAvatar.jsx +++ b/src/components/profilePhotos/BigAvatar.jsx @@ -1,13 +1,13 @@ import React, { useState } from 'react'; -import { FormattedMessage } from 'react-intl'; import { get } from 'lodash-es'; import { useTheme } from '@material-ui/core/styles'; import Chip from '@material-ui/core/Chip'; -import SvgText from '../SvgText'; import EditAvatar from './EditAvatar'; import defaultProfilePhoto from '../../assets/defaultProfile.jpg'; +import { EditOutlined } from '@material-ui/icons'; + export default function BigAvatar({ imageSrc, @@ -22,7 +22,7 @@ export default function BigAvatar({ chipLabel, }) { const theme = useTheme(); - const [avatarHovered, setAvatarHovered] = useState(false); + const [backgroundColor, setBackgroundColor] = useState(theme.palette.primary.main); const [editingAvatar, setEditingAvatar] = useState(false); return ( @@ -141,53 +141,30 @@ export default function BigAvatar({ ))} )} - {editable && ( - setAvatarHovered(true)} - onMouseLeave={() => setAvatarHovered(false)} - onClick={() => setEditingAvatar(true)} + right: 1, + bottom: 1, + width: 40, + height: 40, + borderRadius: '50%', + opacity: 1, + zIndex: 2, + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + backgroundColor: backgroundColor, + }} + // onMouseEnter={() => setBackgroundColor(theme.palette.primary.main)} + // onMouseLeave={() => setBackgroundColor(theme.palette.primary.main+'26')} + onClick={() => setEditingAvatar(true)} > - - - - - - {square ? ( - - ) : ( - - )} - - - - + +
+ )} diff --git a/src/constants/queryKeys.js b/src/constants/queryKeys.js index 61643782d..9debc8571 100644 --- a/src/constants/queryKeys.js +++ b/src/constants/queryKeys.js @@ -52,12 +52,12 @@ export function getUserQueryKey(guid) { return ['user', guid]; } -export function getUserSightingsQueryKey(guid) { - return ['userSightings', guid]; +export function getUserSightingsQueryKey(guid, query, params) { + return ['userSightings', guid, query, params]; } -export function getUserAgsQueryKey(guid) { - return ['userAgs', guid]; +export function getUserAgsQueryKey(guid, params) { + return ['userAgs', guid, params]; } export function getAssetGroupQueryKey(guid) { @@ -80,6 +80,10 @@ export function getSightingTermQueryKey(searchTerm) { return ['sightingQuickSearch', searchTerm]; } +export function getEncounterTermQueryKey(searchTerm) { + return ['encounterQuickSearch', searchTerm]; +} + export function getSocialGroupQueryKey(guid) { return ['socialGroup', guid]; } @@ -100,6 +104,14 @@ export function getSightingFilterQueryKey( return ['sightingFilterSearch', filters, page, rowsPerPage]; } +export function getEncounterFilterQueryKey( + filters, + page, + rowsPerPage, +) { + return ['encounterFilterSearch', filters, page, rowsPerPage]; +} + export function getAuditLogQueryKey(filters, page, rowsPerPage) { return ['auditLogFilterSearch', filters, page, rowsPerPage]; } diff --git a/src/hooks/useFetch.js b/src/hooks/useFetch.js index a77a1ea32..6b466177b 100644 --- a/src/hooks/useFetch.js +++ b/src/hooks/useFetch.js @@ -34,7 +34,6 @@ export default function useFetch({ !queryOptions.disabled, // should this use enabled instead of disabled? I couldn't find anything in the react-query documentation about disabled. // agreed, I think it should be enabled ); - const [statusCode, setStatusCode] = useState(null); const apiUrl = prependHoustonApiUrl ? `${__houston_url__}/api/v1${url}` @@ -50,7 +49,6 @@ export default function useFetch({ responseType, }); const status = response?.status; - setStatusCode(status); if (status === 200) onSuccess(response); return response; }, @@ -68,22 +66,15 @@ export default function useFetch({ if (result?.status === 'loading') { setDisplayedLoading(true); } else { - if (statusCode !== statusCodeFromError) - setStatusCode(statusCodeFromError); if (displayedError !== error) setDisplayedError(error); setDisplayedLoading(false); } - }, [ - error, - result?.status, - statusCodeFromError, - statusCode, - displayedError, - ]); + }, [error, result?.status, statusCodeFromError, displayedError]); return { ...result, - statusCode, + statusCode: + result?.data?.status || result?.error?.response?.status, data: dataAccessor(result), isLoading: displayedLoading, loading: displayedLoading, diff --git a/src/hooks/useOptions.js b/src/hooks/useOptions.js index 9f2aa7e8d..5973aac13 100644 --- a/src/hooks/useOptions.js +++ b/src/hooks/useOptions.js @@ -41,17 +41,26 @@ export default function useOptions() { })) .filter(o => o); - const pipelineStateOptions = [{label:"preparation", value: "preparation"}, - {label: "detection", value: "detection"}, - {label: "curation", value: "curation"}, - {label: "identification", value: "identification"}, + const pipelineStateOptions = [ + { label: 'preparation', value: 'preparation' }, + { label: 'detection', value: 'detection' }, + { label: 'curation', value: 'curation' }, + { label: 'identification', value: 'identification' }, ]; - const stageOptions = [{label:"un_reviewed", value: "un_reviewed"}, - {label: "processed", value: "processed"}, - {label: "failed", value: "failed"}, - {label: "identification", value: "identification"}, - ]; + const stageOptions = [ + { label: 'un_reviewed', value: 'un_reviewed' }, + { label: 'processed', value: 'processed' }, + { label: 'failed', value: 'failed' }, + { label: 'identification', value: 'identification' }, + ]; + + const stateOptions = [ + { label: 'unreviewed', value: 'unreviewed' }, + { label: 'in progress', value: 'in_progress' }, + { label: 'reviewed', value: 'reviewed' }, + { label: 'unidentifiable', value: 'unidentifiable' }, + ]; const booleanChoices = [ { @@ -71,21 +80,30 @@ export default function useOptions() { }, ]; - const socialGroupRolesOptions = data['social_group_roles'].value.map(o => { - return { - label: o.label, - value: o.guid - } - }); - - const relationshipOptions = Object.values(data['relationship_type_roles'].value).map(o => { - return { + const socialGroupRolesOptions = data.social_group_roles.value.map( + o => ({ label: o.label, value: o.guid, - roles: o.roles - } - }); + }), + ); + + const relationshipOptions = Object.values( + data.relationship_type_roles.value, + ).map(o => ({ + label: o.label, + value: o.guid, + roles: o.roles, + })); - return { regionOptions, speciesOptions, pipelineStateOptions, stageOptions, booleanChoices, socialGroupRolesOptions, relationshipOptions }; + return { + regionOptions, + speciesOptions, + pipelineStateOptions, + stageOptions, + booleanChoices, + socialGroupRolesOptions, + relationshipOptions, + stateOptions, + }; }, [loading, error, data]); } diff --git a/src/models/encounter/useEncounterSearchSchemas.js b/src/models/encounter/useEncounterSearchSchemas.js new file mode 100644 index 000000000..a8e8e0909 --- /dev/null +++ b/src/models/encounter/useEncounterSearchSchemas.js @@ -0,0 +1,97 @@ +import useOptions from '../../hooks/useOptions'; +import OptionTermFilter from '../../components/filterFields/OptionTermFilter'; +import PointDistanceFilter from '../../components/filterFields/PointDistanceFilter'; +import SubstringFilter from '../../components/filterFields/SubstringFilter'; +import DateRangeFilter from '../../components/filterFields/DateRangeFilter'; +import useSiteSettings from '../site/useSiteSettings'; +import autogenNameFilter from '../../components/filterFields/autogenNameFilter'; +import useBuildFilter from '../../components/filterFields/useBuildFilter'; + +export default function useSightingSearchSchemas() { + const { regionOptions, speciesOptions, stateOptions } = + useOptions(); + + const { data: siteSettings } = useSiteSettings(); + + const customEncounterFields = + siteSettings['site.custom.customFields.Encounter'].value + .definitions || []; + + const encountersField = useBuildFilter(customEncounterFields); + + return [ + { + id: 'name', + labelId: 'INDIVIDUAL_NAME', + FilterComponent: SubstringFilter, + filterComponentProps: { + filterId: 'name', + queryTerms: ['individualNamesWithContexts.FirstName'], + }, + }, + { + id: 'codexId', + labelId: 'CODEX_ID', + FilterComponent: autogenNameFilter, + filterComponentProps: { + filterId: 'codexId', + queryTerms: ['individualNamesWithContexts'], + }, + }, + { + id: 'species', + labelId: 'SPECIES', + FilterComponent: OptionTermFilter, + filterComponentProps: { + filterId: 'species', + queryTerm: 'taxonomy_guid', + choices: speciesOptions, + }, + }, + { + id: 'match_state', + labelId: 'STATE', + FilterComponent: OptionTermFilter, + filterComponentProps: { + filterId: 'match_state', + queryTerm: 'match_state', + choices: stateOptions, + }, + }, + { + id: 'time', + labelId: 'SIGHTING_DATE_RANGE', + FilterComponent: DateRangeFilter, + filterComponentProps: { queryTerm: 'time', filterId: 'time' }, + }, + { + id: 'locationId', + labelId: 'REGION', + FilterComponent: OptionTermFilter, + filterComponentProps: { + queryTerm: 'locationId', + filterId: 'locationId', + choices: regionOptions, + }, + }, + { + id: 'latlong', + labelId: 'EXACT_LOCATION', + FilterComponent: PointDistanceFilter, + filterComponentProps: { + filterId: 'latlong', + queryTerm: 'location_geo_point', + }, + }, + { + id: 'verbatimLocality', + labelId: 'FREEFORM_LOCATION', + FilterComponent: SubstringFilter, + filterComponentProps: { + filterId: 'verbatimLocality', + queryTerms: ['verbatimLocality'], + }, + }, + ...encountersField, + ]; +} diff --git a/src/models/encounter/useEncounterTermQuery.js b/src/models/encounter/useEncounterTermQuery.js new file mode 100644 index 000000000..341b75e5b --- /dev/null +++ b/src/models/encounter/useEncounterTermQuery.js @@ -0,0 +1,32 @@ +import useFetch from '../../hooks/useFetch'; +import { getEncounterTermQueryKey } from '../../constants/queryKeys'; + +export default function useEncounterTermQuery(searchTerm) { + const query = { + bool: { + minimum_should_match: 1, + should: [ + { + query_string: { + query: `*${searchTerm}*`, + fields: [ + 'verbatimLocality', + 'owner.full_name', + 'locationId_value', + ], + }, + }, + ], + }, + }; + + return useFetch({ + method: 'post', + url: '/encounters/search', + queryKey: getEncounterTermQueryKey(searchTerm), + data: query, + queryOptions: { + enabled: Boolean(searchTerm), + }, + }); +} diff --git a/src/models/encounter/useFilterEncounters.js b/src/models/encounter/useFilterEncounters.js new file mode 100644 index 000000000..82e434fbd --- /dev/null +++ b/src/models/encounter/useFilterEncounters.js @@ -0,0 +1,48 @@ +import { get, partition } from 'lodash-es'; + +import useFetch from '../../hooks/useFetch'; +import { getEncounterFilterQueryKey } from '../../constants/queryKeys'; + +export default function useFilterEncounters({ + queries, + params = {}, +}) { + const [filters, mustNots] = partition( + queries, + q => q.clause === 'filter', + ); + + const filterQueries = filters.map(f => f.query); + const mustNotQueries = mustNots.map(f => f.query); + + const compositeQuery = { + bool: { filter: filterQueries, must_not: mustNotQueries }, + }; + return useFetch({ + method: 'post', + queryKey: getEncounterFilterQueryKey(queries, params), + url: '/encounters/search', + data: compositeQuery, + params: { + limit: 20, + offset: 0, + sort: 'created', + reverse: false, + ...params, + }, + dataAccessor: result => { + const resultCountString = get(result, [ + 'data', + 'headers', + 'x-total-count', + ]); + return { + resultCount: parseInt(resultCountString, 10), + results: get(result, ['data', 'data']), + }; + }, + queryOptions: { + retry: 2, + }, + }); +} diff --git a/src/models/individual/useFilterIndividuals.js b/src/models/individual/useFilterIndividuals.js index 1e43e4b3d..c123b1582 100644 --- a/src/models/individual/useFilterIndividuals.js +++ b/src/models/individual/useFilterIndividuals.js @@ -4,10 +4,11 @@ import useFetch from '../../hooks/useFetch'; import { nestQueries } from '../../utils/elasticSearchUtils'; import { getIndividualFilterQueryKey } from '../../constants/queryKeys'; + export default function useFilterIndividuals({ queries, params = {}, -}) { +}) { const [filters, mustNots] = partition( queries, q => q.clause === 'filter', diff --git a/src/models/users/useGetUserSightings.js b/src/models/users/useGetUserSightings.js index 1b5605531..f257efe64 100644 --- a/src/models/users/useGetUserSightings.js +++ b/src/models/users/useGetUserSightings.js @@ -1,23 +1,37 @@ +import { get } from 'lodash-es'; import useFetch from '../../hooks/useFetch'; import { getUserSightingsQueryKey } from '../../constants/queryKeys'; -export default function useGetUserSightings(userGuid) { +export default function useGetUserSightings(userGuid, params) { const query = { term: { 'owners.guid': userGuid } }; return useFetch({ method: 'post', url: '/sightings/search', - queryKey: getUserSightingsQueryKey(userGuid), + queryKey: getUserSightingsQueryKey(userGuid, query, params), data: query, /* Return up to 20 sightings, most recently reported first */ params: { limit: 20, offset: 0, sort: 'created', - reverse: true, + reverse: false, + ...params, }, queryOptions: { enabled: Boolean(userGuid), + retry: 2, + }, + dataAccessor: result => { + const resultCountString = get(result, [ + 'data', + 'headers', + 'x-total-count', + ]); + return { + resultCount: parseInt(resultCountString, 10), + results: get(result, ['data', 'data']), + }; }, }); } diff --git a/src/models/users/useGetUserUnproccessedAssetGroupSightings.js b/src/models/users/useGetUserUnproccessedAssetGroupSightings.js index fc71d75d5..4cd2954bc 100644 --- a/src/models/users/useGetUserUnproccessedAssetGroupSightings.js +++ b/src/models/users/useGetUserUnproccessedAssetGroupSightings.js @@ -1,3 +1,4 @@ +import { get } from 'lodash-es'; import { getUserAgsQueryKey } from '../../constants/queryKeys'; import useFetch from '../../hooks/useFetch'; @@ -6,12 +7,26 @@ export default function useGetUserUnprocessedAssetGroupSightings( params = {}, ) { return useFetch({ - queryKey: getUserAgsQueryKey(userId), + queryKey: getUserAgsQueryKey(userId, params), url: `/users/${userId}/asset_group_sightings`, params: { limit: 20, offset: 0, ...params, }, + dataAccessor: result => { + const resultCountString = get(result, [ + 'data', + 'headers', + 'x-total-count', + ]); + return { + resultCount: parseInt(resultCountString, 10), + results: get(result, ['data', 'data']), + }; + }, + queryOptions: { + retry: 2, + }, }); } diff --git a/src/pages/changeLog/ChangeLog.jsx b/src/pages/changeLog/ChangeLog.jsx index 0582f02fe..a9e1b6440 100644 --- a/src/pages/changeLog/ChangeLog.jsx +++ b/src/pages/changeLog/ChangeLog.jsx @@ -43,7 +43,7 @@ export default function ChangeLog() { const tableColumns = buildTableColumns(intl); const [inputValue, setInputValue] = useState(''); - const rowsPerPage = 100; + const rowsPerPage = 5; const [searchParams, setSearchParams] = useState({ limit: rowsPerPage, offset: 0, diff --git a/src/pages/controlPanel/ControlPanel.jsx b/src/pages/controlPanel/ControlPanel.jsx index 5e6ae38e4..ff9eb40d0 100644 --- a/src/pages/controlPanel/ControlPanel.jsx +++ b/src/pages/controlPanel/ControlPanel.jsx @@ -3,15 +3,12 @@ import { get } from 'lodash-es'; import { useTheme } from '@material-ui/core'; import Paper from '@material-ui/core/Paper'; -import SplashSettingsIcon from '@material-ui/icons/Home'; import SiteSettingsIcon from '@material-ui/icons/SettingsApplications'; -import PreferencesIcon from '@material-ui/icons/Settings'; import CustomFieldsIcon from '@material-ui/icons/Tune'; import SiteStatusIcon from '@material-ui/icons/Speed'; import UserManagementIcon from '@material-ui/icons/People'; import AdministrationIcon from '@material-ui/icons/Gavel'; import GroupWorkIcon from '@material-ui/icons/GroupWork'; -import AssignmentOutlinedIcon from '@material-ui/icons/AssignmentOutlined'; import ListAltIcon from '@material-ui/icons/ListAlt'; import useDocumentTitle from '../../hooks/useDocumentTitle'; @@ -28,13 +25,6 @@ const adminPages = [ href: '/settings/general', roles: ['is_admin'], }, - // { - // icon: SplashSettingsIcon, - // name: 'front-page-config', - // labelId: 'FRONT_PAGE', - // href: '/settings/front-page', - // roles: ['is_admin'], - // }, { icon: CustomFieldsIcon, name: 'field-management', @@ -75,21 +65,6 @@ const adminPages = [ href: '/settings/actions', roles: ['is_admin'], }, - { - icon: PreferencesIcon, - name: 'preferences', - labelId: 'PREFERENCES', - href: '/settings/preferences', - roles: [ - 'is_admin', - 'is_exporter', - 'is_internal', - 'is_staff', - 'is_user_manager', - 'is_researcher', - 'is_contributor', - ], - }, { icon: ListAltIcon, name: 'change-log', diff --git a/src/pages/dataPage/DataPage.jsx b/src/pages/dataPage/DataPage.jsx new file mode 100644 index 000000000..efc9c3da9 --- /dev/null +++ b/src/pages/dataPage/DataPage.jsx @@ -0,0 +1,341 @@ +import React, { useState } from 'react'; +import { FormattedMessage, useIntl } from 'react-intl'; + +import { FormControl, MenuItem, Select } from '@material-ui/core'; +import { useHistory } from 'react-router-dom'; +import { useTheme } from '@material-ui/core/styles'; +import useGetUserSightings from '../../models/users/useGetUserSightings'; +import useGetUserUnprocessedAssetGroupSightings from '../../models/users/useGetUserUnproccessedAssetGroupSightings'; +// import { formatUserMessage } from '../../utils/formatters'; + +import MainColumn from '../../components/MainColumn'; +import SightingsCard from '../../components/cards/SightingsCard'; + +import CardContainer from '../../components/cards/CardContainer'; +import useGetMe from '../../models/users/useGetMe'; +import Text from '../../components/Text'; +import LoadingScreen from '../../components/LoadingScreen'; + +import useDeleteSighting from '../../models/sighting/useDeleteSighting'; +import useDeleteAssetGroupSighting from '../../models/assetGroupSighting/useDeleteAssetGroupSighting'; +import ConfirmDelete from '../../components/ConfirmDelete'; +import Paginator from '../../components/dataDisplays/Paginator'; + +export default function DataPage() { + const { data: userData, loading: userDataLoading } = useGetMe(); + + const theme = useTheme(); + + const history = useHistory(); + + const rowsPerPagePendingSightings = 10; + const [ + searchParamsPendingSightings, + setSearchParamsPendingSightings, + ] = useState({ + limit: rowsPerPagePendingSightings, + offset: 0, + sort: 'created', + reverse: true, + }); + + const rowsPerPageSightings = 10; + const [searchParamsSightings, setSearchParamsSightings] = useState({ + limit: rowsPerPageSightings, + offset: 0, + sort: 'created', + reverse: true, + }); + + const userId = userData?.guid; + const { data: sightingsDataObject, loading: sightingsLoading } = + useGetUserSightings(userId, searchParamsSightings); + const { + results: sightingsData, + resultCount: resultCountSightings, + } = sightingsDataObject; + + const unapprovedSightingsData = sightingsData?.filter( + sighting => + sighting.match_state === 'unreviewed' || + sighting.match_state === 'in_progress', + ); + + const [selected, setSelected] = React.useState('all_data'); + + const intl = useIntl(); + + const { data: agsDataObject, loading: agsLoading } = + useGetUserUnprocessedAssetGroupSightings( + userId, + searchParamsPendingSightings, + ); + + const { + results: agsData, + resultCount: resultCountPendingSigtings, + } = agsDataObject; + + const { + deleteSighting, + loading: deleteInProgress, + error: deleteSightingError, + onClearError: deleteSightingOnClearError, + vulnerableIndividual, + onClearVulnerableIndividual, + } = useDeleteSighting(); + + const { + deleteAssetGroupSighting, + isLoading: deleteAgsInProgress, + error: deleteAssetGroupSightingError, + onClearError: deleteAsgOnClearError, + } = useDeleteAssetGroupSighting(); + + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [id, setId] = useState(null); + + const [pending, setPending] = useState(''); + + const onClearError = pending + ? deleteAsgOnClearError + : deleteSightingOnClearError; + + if (userDataLoading) return ; + + return ( + + +
+ { + onClearVulnerableIndividual(); + onClearError(); + setDeleteDialogOpen(false); + }} + onDelete={async () => { + let deleteResults; + if (pending) { + deleteResults = await deleteAssetGroupSighting(id); + } else if (vulnerableIndividual) { + deleteResults = await deleteSighting(id, true); + } else { + deleteResults = await deleteSighting(id); + } + const successful = pending + ? deleteResults?.status === 204 + : deleteResults; + if (successful) { + setDeleteDialogOpen(false); + history.push('/'); + } + }} + deleteInProgress={ + pending ? deleteAgsInProgress : deleteInProgress + } + error={ + pending + ? deleteAssetGroupSightingError + : deleteSightingError + } + errorTitleId={ + vulnerableIndividual + ? 'REQUEST_REQUIRES_ADDITIONAL_CONFIRMATION' + : undefined + } + alertSeverity={vulnerableIndividual ? 'warning' : 'error'} + onClearError={onClearError} + messageId={ + vulnerableIndividual + ? 'SIGHTING_DELETE_VULNERABLE_INDIVIDUAL_MESSAGE' + : 'CONFIRM_DELETE_SIGHTING_DESCRIPTION' + } + /> +
+ + + +
+ +
+ +
+ +
+ { + setPending(true); + setId(value); + setDeleteDialogOpen(true); + }} + /> +
+ + + {intl.formatMessage( + { id: 'TotalAccount' }, + { totalAccount: resultCountPendingSigtings }, + )} + +
+ + + { + setPending(false); + setId(value); + setDeleteDialogOpen(true); + }} + /> + +
+ + + {intl.formatMessage( + { id: 'TotalAccount' }, + { totalAccount: resultCountSightings || 0 }, + )} + +
+
+
+
+
+ ); +} diff --git a/src/pages/fieldManagement/RegionManagement.jsx b/src/pages/fieldManagement/RegionManagement.jsx new file mode 100644 index 000000000..9c3b3218c --- /dev/null +++ b/src/pages/fieldManagement/RegionManagement.jsx @@ -0,0 +1,128 @@ +import React, { useEffect, useState } from 'react'; +import { get } from 'lodash-es'; +import { FormattedMessage } from 'react-intl'; +import { useHistory } from 'react-router-dom'; +import MainColumn from '../../components/MainColumn'; +import TreeEditor from './settings/defaultFieldComponents/TreeEditor'; +import useSiteSettings from '../../models/site/useSiteSettings'; +import Button from '../../components/Button'; +import Text from '../../components/Text'; +import SettingsBreadcrumbs from '../../components/SettingsBreadcrumbs'; +import usePutSiteSetting from '../../models/site/usePutSiteSetting'; +import CustomAlert from '../../components/Alert'; + +function getInitialFormState(siteSettings) { + const regions = get( + siteSettings, + ['site.custom.regions', 'value'], + [], + ); + const species = get(siteSettings, ['site.species', 'value'], []); + const relationships = get( + siteSettings, + ['relationship_type_roles', 'value'], + [], + ); + const socialGroups = get( + siteSettings, + ['social_group_roles', 'value'], + [], + ); + + return { regions, species, relationships, socialGroups }; +} + +export default function RegionManagement() { + const [formSettings, setFormSettings] = useState(null); + const { data: siteSettings } = useSiteSettings(); + const history = useHistory(); + + const { + mutate: putSiteSetting, + error: putError, + loading, + clearError, + } = usePutSiteSetting(); + + const onClose = () => { + clearError(); + history.push('/settings/fields'); + }; + + useEffect( + () => setFormSettings(getInitialFormState(siteSettings)), + [siteSettings], + ); + + const tree = get(formSettings, ['regions', 'locationID'], []); + + return ( + + + + { + const newRegions = { + ...get(formSettings, 'regions', {}), + locationID, + }; + setFormSettings({ + ...formSettings, + regions: newRegions, + }); + }} + /> +
+ + +
+
+ {putError ? ( + + ) : null} +
+
+ ); +} \ No newline at end of file diff --git a/src/pages/fieldManagement/settings/DefaultFieldTable.jsx b/src/pages/fieldManagement/settings/DefaultFieldTable.jsx index fa0ae1fde..465a520e1 100644 --- a/src/pages/fieldManagement/settings/DefaultFieldTable.jsx +++ b/src/pages/fieldManagement/settings/DefaultFieldTable.jsx @@ -11,7 +11,7 @@ import DataDisplay from '../../../components/dataDisplays/DataDisplay'; import ActionIcon from '../../../components/ActionIcon'; import Text from '../../../components/Text'; import categoryTypes from '../../../constants/categoryTypes'; -import { RegionEditor } from './defaultFieldComponents/Editors'; +// import { RegionEditor } from './defaultFieldComponents/Editors'; import RelationshipEditor from './defaultFieldComponents/RelationshipEditor'; import SocialGroupsEditor from './defaultFieldComponents/SocialGroupsEditor'; import { cellRendererTypes } from '../../../components/dataDisplays/cellRenderers'; @@ -22,14 +22,13 @@ const configurableFields = [ backendPath: 'site.species', labelId: 'SPECIES', type: categoryTypes.sighting, - // Editor: SpeciesEditor, }, { id: 'region', backendPath: 'site.custom.regions', labelId: 'REGION', type: categoryTypes.sighting, - Editor: RegionEditor, + // Editor: RegionEditor, }, { id: 'relationship', @@ -84,14 +83,49 @@ export default function DefaultFieldTable({ siteSettings }) { [siteSettings], ); + const onCloseEditor = () => { + clearError(); + setEditField(null); + }; + + const onClose = () => { + setFormSettings(getInitialFormState(siteSettings)); + onCloseEditor(); + }; + + const onSubmit = async () => { + if (editField?.id === 'region') { + console.log('formSettings.regions', formSettings.regions); + const response = await putSiteSetting({ + property: editField.backendPath, + data: formSettings.regions, + }); + if (response?.status === 200) onCloseEditor(); + } + if (editField?.id === 'relationship') { + const response = await putSiteSetting({ + property: editField.backendPath, + data: formSettings.relationships, + }); + if (response?.status === 200) onCloseEditor(); + } + if (editField?.id === 'socialGroups') { + const response = await putSiteSetting({ + property: editField.backendPath, + data: formSettings.socialGroups, + }); + if (response?.status === 200) onCloseEditor(); + } + }; + const tableColumns = [ { name: 'labelId', label: intl.formatMessage({ id: 'LABEL' }), options: { - customBodyRender: labelId => ( - - ), + customBodyRender: ( + labelId, //eslint-disable-line + ) => , }, }, { @@ -103,15 +137,20 @@ export default function DefaultFieldTable({ siteSettings }) { name: 'actions', label: intl.formatMessage({ id: 'ACTIONS' }), options: { - customBodyRender: (_, field) => ( + customBodyRender: ( + _, + field, //eslint-disable-line + ) => ( { - if(field.id === 'species'){ + if (field.id === 'species') { history.push('/settings/fields/species'); - }else { + } else if (field.id === 'region') { + history.push('/settings/fields/regions'); + } else { setEditField(field); - } + } }} /> ), @@ -119,11 +158,6 @@ export default function DefaultFieldTable({ siteSettings }) { }, ]; - const onCloseEditor = () => { - clearError(); - setEditField(null); - }; - return ( {editField && ( @@ -131,33 +165,8 @@ export default function DefaultFieldTable({ siteSettings }) { siteSettings={siteSettings} formSettings={formSettings} setFormSettings={setFormSettings} - onClose={() => { - setFormSettings(getInitialFormState(siteSettings)); - onCloseEditor(); - }} - onSubmit={async () => { - if (editField?.id === 'region') { - const response = await putSiteSetting({ - property: editField.backendPath, - data: formSettings.regions, - }); - if (response?.status === 200) onCloseEditor(); - } - if (editField?.id === 'relationship') { - const response = await putSiteSetting({ - property: editField.backendPath, - data: formSettings.relationships, - }); - if (response?.status === 200) onCloseEditor(); - } - if (editField?.id === 'socialGroups') { - const response = await putSiteSetting({ - property: editField.backendPath, - data: formSettings.socialGroups, - }); - if (response?.status === 200) onCloseEditor(); - } - }} + onClose={onClose} + onSubmit={onSubmit} > {error ? ( { const newLocationID = leaf.locationID - ? updateTree(leaf.locationID, leafId, newLeafName) + ? updateTree( + leaf.locationID, + leafId, + newLeafName, + placeholderOnly, + ) : undefined; - const newLeaf = { ...leaf, locationID: newLocationID }; - if (newLeaf.id === leafId) newLeaf.name = newLeafName; + const newLeaf = { + ...leaf, + locationID: newLocationID, + }; + if (newLeaf.id === leafId) { + newLeaf.name = newLeafName; + newLeaf.placeholderOnly = placeholderOnly; + } return newLeaf; }); } const Leaf = function ({ level, data, root, onChange, children }) { + const [placeholderOnly, setPlaceholderOnly] = useState( + data.placeholderOnly || false, + ); return (
{ - onChange(updateTree(root, data.id, newName)); + onChange( + updateTree(root, data.id, newName, placeholderOnly), + ); }} value={get(data, 'name')} - autoFocus + // autoFocus InputProps={{ endAdornment: ( { onChange(addLeaf(root, data.id)); }} > - + { onChange(deleteFromTree(root, data.id)); }} /> ), + startAdornment: ( + + { + const newPlaceholderOnly = !placeholderOnly; + setPlaceholderOnly(newPlaceholderOnly); + onChange( + updateTree( + root, + data.id, + data.name, + newPlaceholderOnly, + ), + ); + }} + name="startCheckbox" + color="primary" + /> + } + /> + + ), }} /> {children} @@ -127,17 +171,18 @@ export default function TreeEditor({
- @@ -153,4 +198,4 @@ export default function TreeEditor({
); -} +} \ No newline at end of file diff --git a/src/pages/home/HomeBreadcrumbs.jsx b/src/pages/home/HomeBreadcrumbs.jsx new file mode 100644 index 000000000..6a3166dda --- /dev/null +++ b/src/pages/home/HomeBreadcrumbs.jsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { useTheme } from '@material-ui/core/styles'; +import Breadcrumbs from '@material-ui/core/Breadcrumbs'; +import Link from '../../components/Link'; +import Text from '../../components/Text'; + + +export default function HomeBreadcrumbs({currentPageText, currentPageTextId}) { + const theme = useTheme(); + + const linkStyles = { + color: theme.palette.text.secondary, + }; + return ( + + + + + {currentPageText} + + ); +} \ No newline at end of file diff --git a/src/pages/individualGallery/components/DerivedAnnotatedPhotograph.jsx b/src/pages/individualGallery/components/DerivedAnnotatedPhotograph.jsx index 975dd00c0..91b47941d 100644 --- a/src/pages/individualGallery/components/DerivedAnnotatedPhotograph.jsx +++ b/src/pages/individualGallery/components/DerivedAnnotatedPhotograph.jsx @@ -60,6 +60,7 @@ export default function DerivedAnnotatedPhotograph(props) { if (imageWidth && imageHeight && !isClamped) { return ( ({ + root: { + display: 'flex', + flexWrap: 'wrap', + // width: '100', + }, + image: { + position: 'relative', + height: 200, + [theme.breakpoints.down('xs')]: { + width: '100% !important', // Overrides inline-style + height: 100, + }, + '&:hover, &$focusVisible': { + zIndex: 1, + '& $imageBackdrop': { + opacity: 0.05, + }, + '& $imageMarked': { + opacity: 0, + }, + '& $imageTitle': { + border: '4px solid currentColor', + }, + }, + }, + focusVisible: {}, + imageButton: { + position: 'absolute', + left: 0, + right: 0, + top: 0, + bottom: 0, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + color: theme.palette.common.white, + }, + imageSrc: { + position: 'absolute', + left: 0, + right: 0, + top: 0, + bottom: 0, + backgroundSize: 'cover', + backgroundPosition: 'center 40%', + }, + imageBackdrop: { + position: 'absolute', + left: 0, + right: 0, + top: 0, + bottom: 0, + backgroundColor: theme.palette.common.black, + opacity: 0.7, + transition: theme.transitions.create('opacity'), + }, + imageBackdropFocus: { + position: 'absolute', + left: 0, + right: 0, + top: 0, + bottom: 0, + backgroundColor: theme.palette.common.black, + opacity: 0.05, + transition: theme.transitions.create('opacity'), + }, + imageTitle: { + position: 'relative', + padding: `${theme.spacing(2)}px ${theme.spacing(4)}px ${ + theme.spacing(1) + 6 + }px`, + }, + imageMarked: { + height: 3, + width: 18, + backgroundColor: theme.palette.common.white, + position: 'absolute', + bottom: -2, + left: 'calc(50% - 9px)', + transition: theme.transitions.create('opacity'), + }, +})); + +export default function MyImageButton({ + title, + width, + height, + url, + isSelected, + onClick, +}) { + const classes = useStyles(); + + return ( +
+ + {title} + + + + + {title} + + + + +
+ ); +} diff --git a/src/pages/match/ImageCard.jsx b/src/pages/match/ImageCard.jsx index 6caa94b38..176155cdd 100644 --- a/src/pages/match/ImageCard.jsx +++ b/src/pages/match/ImageCard.jsx @@ -1,6 +1,8 @@ -import React, { useMemo } from 'react'; -import { get } from 'lodash-es'; +import React, { useEffect, useMemo, useState } from 'react'; +import _, { get } from 'lodash-es'; +import ArrowBackIosIcon from '@material-ui/icons/ArrowBackIos'; +import ArrowForwardIosIcon from '@material-ui/icons/ArrowForwardIos'; import { formatSpecifiedTime } from '../../utils/formatters'; import useSiteSettings from '../../models/site/useSiteSettings'; import AnnotatedPhotograph from '../../components/AnnotatedPhotograph'; @@ -9,8 +11,18 @@ import Link from '../../components/Link'; import Card from '../../components/cards/Card'; import LocationIdViewer from '../../components/fields/view/LocationIdViewer'; import DataLineItem from './DataLineItem'; +import Button from '../../components/Button'; +import MyImageButton from './ImageButton'; +import DerivedAnnotatedPhotograph from '../individualGallery/components/DerivedAnnotatedPhotograph'; -export default function ImageCard({ titleId, annotation, heatmapon, heatmapurl, left }) { +export default function ImageCard({ + titleId, + annotation, + heatmapon, + heatmapurl, + left, + allData, +}) { const { data: siteSettings, loading } = useSiteSettings(); const regionChoices = useMemo( @@ -35,37 +47,139 @@ export default function ImageCard({ titleId, annotation, heatmapon, heatmapurl, annotation?.sighting_time_specificity, ); + const getSelectedIndexByAnnotation = () => { + if (_.isNil(annotation) || _.isEmpty(annotation)) { + return 0; + } + const index = allData.findIndex(data => { + if (data?.guid === annotation?.guid) { + return true; + } + return false; + }); + if (index === -1) { + return 0; + } + return index; + }; + + const getAnnotationByIndex = index => { + if (_.isNil(allData) || _.isEmpty(allData)) { + return annotation; + } + if (index < 0 || index > allData.length - 1) { + return allData[0]; + } + return allData[index]; + }; + + const [selectedIndex, setSelectedIndex] = useState(0); + + useEffect(() => { + const index = getSelectedIndexByAnnotation(); + setSelectedIndex(index); + }, [annotation?.image_url, annotation?.bounds]); + + const getDisplayedIndex = index => { + const arr = _.range(0, allData.length); + if (index === 0) { + return _.take(arr, 3); + } + if (index === arr.length - 1) { + return _.takeRight(arr, 3); + } + return _.slice(arr, index - 1, index + 2); + }; + return ( - { - heatmapon && heatmapurl ? ( - + ) : ( +
+ - ) - : ( - - ) - } - + /> + {allData.length > 1 && ( +
+
+ )} +
+ )} +
({ ...data, - individual_first_name : data.individual_first_name || '-' - })) + individual_first_name: data.individual_first_name || '-', + })); return ( { + encounters.push(data); + return null; + }); + const annotations = []; + encounters.map(data => { + annotations.push(...data.annotations); + return null; + }); + return annotations; + } + + function getAllAnnotationsFromEncounter(encounterData) { + const annotations = encounterData?.annotations; + return annotations; + } + + const getAndDeduplicateAnnotations = ( + individualData, + encounterData, + ) => { + const annotationsFromIndividual = + getAllAnnotationsFromIndividual(individualData) || []; + const annotationsFromEncounter = + getAllAnnotationsFromEncounter(encounterData) || []; + + return ( + _.uniqWith( + [...annotationsFromIndividual, ...annotationsFromEncounter], + _.isEqual, + ) || [] + ); + }; + + const queryAllData = getAndDeduplicateAnnotations( + individualData_query, + encounterData_query, + ); + const matchAllData = getAndDeduplicateAnnotations( + individualData_match, + encounterData_match, + ); + const matchCandidates = useMemo(() => { const hotspotterAnnotationScores = get( selectedQueryAnnotation, @@ -273,6 +334,7 @@ export default function MatchSighting() { heatmapon={checked} heatmapurl={heatMapUrl} left + allData={queryAllData} /> { - const valueKey = get(field, 'name'); - const defaultValue = get(field, 'defaultValue'); - memo[valueKey] = get(data, valueKey, defaultValue); - return memo; - }, {}); -} - -export default function Preferences() { - useDocumentTitle('PREFERENCES'); - - const { data } = useGetMe(); - - const [deactivating, setDeactivating] = useState(false); - - const { - mutate: replaceUserProperty, - loading, - error, - clearError, - } = useReplaceUserProperty(); - const schemas = useNotificationSettingsSchemas(); - - const [formValues, setFormValues] = useState({}); - useEffect(() => { - const initialValues = getInitialFormValues(schemas, data); - - setFormValues(prevFormValues => ({ - ...initialValues, - ...prevFormValues, - })); - }, [schemas, data]); - - const backendValues = useMemo( - () => getInitialFormValues(schemas, data), - [schemas, data], - ); - - return ( - - { - clearError(); - }} - errorMessage={error} - /> - {deactivating && ( - setDeactivating(false)} - userData={data} - deactivatingSelf - /> - )} - - - - - - - {schemas.map(notificationField => { - const fieldKey = get(notificationField, 'name'); - const fieldValue = get(formValues, fieldKey); - const backendValue = get(backendValues, fieldKey); - const valueHasChanged = fieldValue !== backendValue; - - return ( - -
- { - setFormValues({ - ...formValues, - [fieldKey]: !fieldValue, - }); - }} - minimalLabels - /> - {valueHasChanged && ( -
-
- )} -
-
- ); - })} -
-
- - - - -
) : null} - - {optional && sightingType ? ( + {optional && sightingType ? ( -