diff --git a/src/components/AnnotationCard/AnnotationCard.js b/src/components/AnnotationCard/AnnotationCard.js index 6dba88fc..5c5e0cbc 100644 --- a/src/components/AnnotationCard/AnnotationCard.js +++ b/src/components/AnnotationCard/AnnotationCard.js @@ -25,7 +25,7 @@ import DocumentAnnotationsContext from '../../contexts/DocumentAnnotationsContex import DocumentFiltersContext from '../../contexts/DocumentFiltersContext'; function addHoverEventListenersToAllHighlightedText() { - // console.log('annotation-highlighted-text', $('.annotation-highlighted-text')); + $('.annotation-highlighted-text').on('mouseover', (e) => { // highlighting all every piece of the annotation a different color by setting it to active $(`.annotation-highlighted-text[annotation-id='${$(e.target).attr('annotation-id')}']`).addClass('active'); @@ -184,6 +184,9 @@ function AnnotationCard({ newAnnotationData.modified = new Date(); newAnnotationData.new = false; newAnnotationData.editing = false; + $($(`#document-content-container span[annotation-id='${newAnnotationData._id}']`).get(-1)).prepend(""); + const annotationEnding = $(`#document-content-container span[annotation-id='${annotation._id}'] .annotation-ending-marker`); + newAnnotationData.position.height = (annotationEnding.offset().top - newAnnotationData.position.top) + 18; setSavingAnnotation(false); // after setting the annotation data we need to reset the "new" data back to null setNewAnnotationTags(null); diff --git a/src/components/Dashboard/DashboardAnnotationList/DashboardAnnotationList.js b/src/components/Dashboard/DashboardAnnotationList/DashboardAnnotationList.js new file mode 100644 index 00000000..a07adf4a --- /dev/null +++ b/src/components/Dashboard/DashboardAnnotationList/DashboardAnnotationList.js @@ -0,0 +1,148 @@ +/* eslint-disable no-underscore-dangle */ +import React, { useState, useEffect } from 'react'; +import Link from 'next/link'; +import { + Badge, Card, Col, ListGroup, Row, Tab, Tabs, +} from 'react-bootstrap'; +import { format } from 'date-fns'; +import LoadingSpinner from '../../LoadingSpinner'; +import { getSharedAnnotations, getOwnAnnotations } from '../../../utils/annotationUtil'; +import { getGroupNameById } from '../../../utils/groupUtil'; +import { FirstNameLastInitial } from '../../../utils/nameUtil'; + +const DashboardAnnotationList = ({ + session, + alerts, + setAlerts, + tab, + mode, +}) => { + const [groupState, setGroupState] = useState({}); + const [key, setKey] = useState(tab || 'mine'); + const [listLoading, setListLoading] = useState(true); + const [annotations, setAnnotations] = useState([]); + const limit = mode === 'dashboard' ? 10 : undefined; + + useEffect(() => { + async function fetchData() { + if (session && (session.user.groups || session.user.id)) { + if (key === 'shared') { + setAnnotations( + await getSharedAnnotations(session.user.groups, limit) + .then(setListLoading(false)) + .catch((err) => setAlerts([...alerts, { text: err.message, variant: 'danger' }])), + ); + } else if (key === 'mine') { + setAnnotations( + await getOwnAnnotations(session.user.id, limit) + .then(setListLoading(false)) + .catch((err) => setAlerts([...alerts, { text: err.message, variant: 'danger' }])), + ); + } + } + } + fetchData(); + }, [key]); + + + useEffect(() => { + if (annotations) { + const fetchGroupState = async () => { + annotations.map((annotation) => annotation.permissions.groups.map(async (group) => { + if (!groupState[group]) { + setGroupState({ ...groupState, [group]: await getGroupNameById(group) }); + } + })); + }; + fetchGroupState(); + } + }, [annotations, groupState]); + + return ( + + + + {mode === 'dashboard' && ( + Annotations + )} + {mode === 'list' && ( + <>Annotations + )} + + setKey(k)} + > + + + + + {listLoading && ( + + )} + {!listLoading && annotations && annotations.length > 0 && ( + <> + + {annotations.sort((a, b) => new Date(b.created) - new Date(a.created)).map( + (annotation) => ( + + + + + {annotation.target.selector.exact} + + + + {format(new Date(annotation.created), 'Ppp')} + + + + + {annotation.body.value} + + + + + + {annotation.target.document.title} + {' ('} + {FirstNameLastInitial(annotation.creator.name)} + ) + {annotation.permissions.groups + && annotation.permissions.groups.length > 0 && ( + + {groupState[annotation.permissions.groups.sort()[0]]} + + )} + + + + + ), + )} + + {mode === 'dashboard' && ( + + + {`See all ${key === 'shared' ? key : 'created'} annotations...`} + + + )} + + )} + {!listLoading && (!annotations || annotations.length === 0) && ( + + {`You have no ${key === 'shared' ? key : 'created'} annotations.`} + + )} + + ); +}; + +export default DashboardAnnotationList; diff --git a/src/components/Dashboard/DashboardAnnotationList/index.js b/src/components/Dashboard/DashboardAnnotationList/index.js new file mode 100644 index 00000000..0ea184ef --- /dev/null +++ b/src/components/Dashboard/DashboardAnnotationList/index.js @@ -0,0 +1,3 @@ +import DashboardAnnotationList from './DashboardAnnotationList'; + +export default DashboardAnnotationList; diff --git a/src/components/Dashboard/DashboardGroupList/DashboardGroupList.js b/src/components/Dashboard/DashboardGroupList/DashboardGroupList.js index 0f23a86a..3fb7f1ce 100644 --- a/src/components/Dashboard/DashboardGroupList/DashboardGroupList.js +++ b/src/components/Dashboard/DashboardGroupList/DashboardGroupList.js @@ -12,8 +12,8 @@ const DashboardGroupList = ({ const [groups, setGroups] = useState([]); useEffect(() => { - if (session && !deepEqual(session.user.groups, groups)) { - setGroups(session.user.groups); + if (session && !deepEqual(session.user.groups.slice(0, 10), groups)) { + setGroups(session.user.groups.slice(0, 10)); } }, [session]); diff --git a/src/components/Document/Document.js b/src/components/Document/Document.js index 573ae84a..6c793095 100644 --- a/src/components/Document/Document.js +++ b/src/components/Document/Document.js @@ -192,15 +192,17 @@ export default class Document extends React.Component { // side channel and then add position data to the annotation object. $($(`#document-content-container span[annotation-id='${annotation._id}']`).get(0)) .prepend(""); + $($(`#document-content-container span[annotation-id='${annotation._id}']`).get(-1)) + .append(""); // so now that we have added the beginning marker we are going to get // the position of the begginning marker then remove it from the dom - const annotationBeginning = $( - `#document-content-container span[annotation-id='${annotation._id}'] .annotation-beginning-marker`, - ); + const annotationBeginning = $(`#document-content-container span[annotation-id='${annotation._id}'] .annotation-beginning-marker`); + const annotationEnding = $(`#document-content-container span[annotation-id='${annotation._id}'] .annotation-ending-marker`); if (annotationBeginning.get(0) === undefined) { setAlerts([...alerts, { text: 'unable to annotate a piece of text', variant: 'danger' }]); } else { const annotationBeginningPosition = annotationBeginning.offset(); + const annotationEndingPosition = annotationEnding.offset(); // this takes into account if the user was scrolling through the document // as the it was being populated with annotations annotationBeginningPosition.top += $('#document-container').scrollTop(); @@ -214,6 +216,7 @@ export default class Document extends React.Component { { left: annotationBeginningPosition.left, top: annotationBeginningPosition.top, + height: (annotationEndingPosition.top - annotationBeginningPosition.top) + 18, }, ...annotation, }, @@ -225,6 +228,7 @@ export default class Document extends React.Component { { left: annotationBeginningPosition.left, top: annotationBeginningPosition.top, + height: (annotationEndingPosition.top - annotationBeginningPosition.top) + 18, }, ...annotation, }, diff --git a/src/components/HeatMap/HeatMap.js b/src/components/HeatMap/HeatMap.js new file mode 100644 index 00000000..33b8d7ae --- /dev/null +++ b/src/components/HeatMap/HeatMap.js @@ -0,0 +1,89 @@ +/* eslint-disable no-underscore-dangle */ +/* eslint-disable guard-for-in */ +/* eslint-disable no-restricted-syntax */ +import React, { useState, useContext, useEffect } from 'react'; +import $ from 'jquery'; +import { + Nav, + Row, + Col, + Navbar, + Breadcrumb, + Container, + Button, + OverlayTrigger, + Popover, + Form, + Card, + ButtonGroup, + Badge, + Spinner, +} from 'react-bootstrap'; + +import DocumentFiltersContext from '../../contexts/DocumentFiltersContext'; +import DocumentAnnotationsContext from '../../contexts/DocumentAnnotationsContext'; + + +function HeatMap() { + const lineHeight = 18; + const scaleFactor = $('#document-container').height() / $('#document-card-container').height(); + const minStrokeHeight = 1; + const offsetTop = $('#document-container').offset() === undefined ? 0 : $('#document-container').offset().top; + const grandularity = lineHeight * scaleFactor >= minStrokeHeight ? lineHeight : Math.ceil(minStrokeHeight / scaleFactor); + const [channelAnnotations] = useContext(DocumentAnnotationsContext); + const [documentFilters] = useContext(DocumentFiltersContext); + const n = (Math.ceil($('#document-card-container').height() / grandularity)); + const map = new Array(isNaN(n) ? 0 : n); + + for (const side in channelAnnotations) { + if (channelAnnotations[side] !== null) { + for (const anno of channelAnnotations[side]) { + if (documentFilters.annotationIds[side] === null || documentFilters.annotationIds[side].includes(anno._id)) { + if (anno.position.height !== undefined) { + // now we have to convert the annotations position and height into starting and ending indexs for the map + let startIndex = Math.floor((anno.position.top - offsetTop) / grandularity); + startIndex = startIndex < 0 ? 0 : startIndex; + const endIndex = Math.floor((anno.position.top + anno.position.height - offsetTop) / grandularity); + for (let i = startIndex; i <= endIndex; i += 1) { + if (map[i] === undefined) { + map[i] = 0; + } + map[i] += 1; + } + } + } + } + } + } + + + return ( + <> +
+ {map.map((v, i) =>
)} +
+ + + + ); +} + +export default HeatMap; diff --git a/src/components/HeatMap/index.js b/src/components/HeatMap/index.js new file mode 100644 index 00000000..501f4411 --- /dev/null +++ b/src/components/HeatMap/index.js @@ -0,0 +1,3 @@ +import HeatMap from './HeatMap'; + +export default HeatMap; diff --git a/src/components/SecondNavbar/SecondNavbar.js b/src/components/SecondNavbar/SecondNavbar.js index a31a26b4..c9677051 100644 --- a/src/components/SecondNavbar/SecondNavbar.js +++ b/src/components/SecondNavbar/SecondNavbar.js @@ -1,8 +1,8 @@ import { - Nav, Row, Col, Navbar, Breadcrumb, Container, Button, OverlayTrigger, Popover, Form, Card, ButtonGroup, + Nav, Row, Col, Navbar, Breadcrumb, Container, } from 'react-bootstrap'; import { - InfoSquare, Filter, ShieldFillCheck, ChatLeftQuoteFill, PeopleFill, BookmarkFill, CalendarEventFill, + InfoSquare, } from 'react-bootstrap-icons'; import FilterPopover from '../FilterPopover'; @@ -20,10 +20,10 @@ const SecondNavbar = ({