diff --git a/frontend/lit/NestedTag.js b/frontend/lit/NestedTag.js index 8e2d038488..9f7c29a70a 100644 --- a/frontend/lit/NestedTag.js +++ b/frontend/lit/NestedTag.js @@ -1,5 +1,6 @@ import React from "react"; import ReactDOM from "react-dom"; +import {getReferenceTagListUrl} from "shared/utils/urls"; import PaginatedReferenceList from "./components/PaginatedReferenceList"; @@ -61,6 +62,10 @@ class NestedTag { }; ReactDOM.render(, el); } + + get_list_link() { + return getReferenceTagListUrl(this.assessment_id, this.data.pk); + } } export default NestedTag; diff --git a/frontend/lit/Reference.js b/frontend/lit/Reference.js index 9a8332e165..62b3361ffb 100644 --- a/frontend/lit/Reference.js +++ b/frontend/lit/Reference.js @@ -8,6 +8,7 @@ class Reference { this.data = data; this._quickSearchText = `${data.title}-${data.year}-${data.authors}-${data.authors_short}`.toLowerCase(); this.tags = data.tags.map(tagId => tagtree.dict[tagId]); + this.userTags = data.user_tags ? data.user_tags.map(tagId => tagtree.dict[tagId]) : []; } static get_detail_url(id, subtype) { diff --git a/frontend/lit/ReferenceTreeBrowse/Main.js b/frontend/lit/ReferenceTreeBrowse/Main.js index 74876e0370..30755c4e44 100644 --- a/frontend/lit/ReferenceTreeBrowse/Main.js +++ b/frontend/lit/ReferenceTreeBrowse/Main.js @@ -72,15 +72,17 @@ class ReferenceTreeMain extends Component { ) : store.selectedTag === null ? (

Available references

) : ( -

- - {selectedReferences && selectedReferences.length > 0 - ? `${filteredReferences.length} references tagged:` - : "References tagged:"} - +
+

+ + {selectedReferences && selectedReferences.length > 0 + ? `${filteredReferences.length} references tagged:` + : "References tagged:"} + +

{store.selectedTag.get_full_name()} {yearText} -

+ )}
diff --git a/frontend/lit/TagReferences/Main.js b/frontend/lit/TagReferences/Main.js index 2cd454a70f..239a7a01cb 100644 --- a/frontend/lit/TagReferences/Main.js +++ b/frontend/lit/TagReferences/Main.js @@ -38,7 +38,10 @@ class TagReferencesMain extends Component { render() { const {store} = this.props, selectedReferencePk = store.selectedReference ? store.selectedReference.data.pk : null, - selectedReferenceTags = store.selectedReferenceTags ? store.selectedReferenceTags : []; + selectedReferenceTags = store.selectedReferenceTags ? store.selectedReferenceTags : [], + selectedReferenceUserTags = store.selectedReferenceUserTags + ? store.selectedReferenceUserTags + : []; return (
@@ -66,7 +69,11 @@ class TagReferencesMain extends Component { {ref.tags.length > 0 ? ( ) : null}

@@ -118,9 +125,32 @@ class TagReferencesMain extends Component { {selectedReferenceTags.map((tag, i) => ( store.removeTag(tag)}> + onClick={ + store.config.conflict_resolution + ? null + : () => store.removeTag(tag) + }> + {this.state.showFullTag + ? tag.get_full_name() + : tag.data.name} + + ))} + {selectedReferenceUserTags.map((tag, i) => ( + store.removeTag(tag) + : null + }> {this.state.showFullTag ? tag.get_full_name() : tag.data.name} @@ -144,13 +174,13 @@ class TagReferencesMain extends Component { extraActions={[
store.removeAllTags()}>  Remove all tags
,
{ this.showFullTag.toggle(); this.setState({showFullTag: this.showFullTag.value}); @@ -163,7 +193,7 @@ class TagReferencesMain extends Component { store.config.instructions.length > 0 ? (
store.setInstructionsModal(true)}>  View instructions
@@ -202,7 +232,7 @@ class TagReferencesMain extends Component { store.addTag(tag)} - showTagHover={true} + showTagHoverAdd={true} />
el.data.pk === tag.data.pk) + !_.find( + this.config.conflict_resolution + ? this.selectedReferenceUserTags + : this.selectedReferenceTags, + el => el.data.pk === tag.data.pk + ) ) { - this.selectedReferenceTags.push(tag); + this.config.conflict_resolution + ? this.selectedReferenceUserTags.push(tag) + : this.selectedReferenceTags.push(tag); } } @action.bound removeTag(tag) { - _.remove(this.selectedReferenceTags, el => el.data.pk === tag.data.pk); + _.remove( + this.config.conflict_resolution + ? this.selectedReferenceUserTags + : this.selectedReferenceTags, + el => el.data.pk === tag.data.pk + ); } @action.bound saveAndNext() { const payload = { pk: this.selectedReference.data.pk, - tags: this.selectedReferenceTags.map(tag => tag.data.pk), + tags: this.config.conflict_resolution + ? this.selectedReferenceUserTags.map(tag => tag.data.pk) + : this.selectedReferenceTags.map(tag => tag.data.pk), }, success = () => { const $el = $(this.saveIndicatorElement), @@ -61,9 +77,11 @@ class Store { $el.fadeIn().fadeOut({ complete: () => { this.selectedReference.tags = toJS(this.selectedReferenceTags); + this.selectedReference.userTags = toJS(this.selectedReferenceUserTags); this.references.splice(index, 1, toJS(this.selectedReference)); this.selectedReference = null; this.selectedReferenceTags = null; + this.selectedReferenceUserTags = null; if (this.references.length > index + 1) { this.changeSelectedReference(this.references[index + 1]); } else { @@ -82,7 +100,9 @@ class Store { ).fail(failure); } @action.bound removeAllTags() { - this.selectedReferenceTags = []; + this.config.conflict_resolution + ? (this.selectedReferenceUserTags = []) + : (this.selectedReferenceTags = []); } @action.bound setSaveIndicatorElement(el) { this.saveIndicatorElement = el; diff --git a/frontend/lit/components/Reference.js b/frontend/lit/components/Reference.js index 847dde0871..64c5cb5c5b 100644 --- a/frontend/lit/components/Reference.js +++ b/frontend/lit/components/Reference.js @@ -111,8 +111,8 @@ class Reference extends Component { authors = data.authors || data.authors_short || reference.NO_AUTHORS_TEXT, year = data.year || "", actionItems = [ - , , + , , , ].concat(extraActions); @@ -155,6 +155,7 @@ class Reference extends Component { {data.abstract ? (
1500 ? {height: "45vh"} : null} dangerouslySetInnerHTML={ keywordDict ? {__html: markKeywords(data.abstract, keywordDict)} @@ -166,9 +167,10 @@ class Reference extends Component {

{tags.map((tag, i) => ( + className="refTag mt-1"> {tag.get_full_name()} ))} diff --git a/frontend/lit/components/TagTree.js b/frontend/lit/components/TagTree.js index e2ceedecac..fff72fa976 100644 --- a/frontend/lit/components/TagTree.js +++ b/frontend/lit/components/TagTree.js @@ -13,11 +13,7 @@ class TagNode extends Component { }; } render() { - const {tag, showReferenceCount, handleOnClick, selectedTag, showTagHover} = this.props, - tagClass = - tag === selectedTag - ? "d-flex nestedTag selected align-items-center" - : "d-flex nestedTag align-items-center", + const {tag, showReferenceCount, handleOnClick, selectedTag, showTagHoverAdd} = this.props, hasChildren = tag.children.length > 0, expanderIcon = this.state.expanded ? "fa-minus" : "fa-plus", toggleExpander = e => { @@ -29,7 +25,9 @@ class TagNode extends Component { return ( <> -

handleOnClick(tag)}> +
handleOnClick(tag)}>
@@ -41,7 +39,11 @@ class TagNode extends Component { ) : null}
-
+
{tag.data.name} {showReferenceCount ? ` (${tag.get_references_deep().length})` : null} @@ -56,7 +58,7 @@ class TagNode extends Component { handleOnClick={handleOnClick} showReferenceCount={showReferenceCount} selectedTag={selectedTag} - showTagHover={showTagHover} + showTagHoverAdd={showTagHoverAdd} /> )) : null} @@ -69,7 +71,7 @@ TagNode.propTypes = { handleOnClick: PropTypes.func.isRequired, showReferenceCount: PropTypes.bool.isRequired, selectedTag: PropTypes.object, - showTagHover: PropTypes.bool, + showTagHoverAdd: PropTypes.bool, }; @observer @@ -82,7 +84,7 @@ class TagTree extends Component { selectedTag, untaggedHandleClick, untaggedCount, - showTagHover, + showTagHoverAdd, } = this.props; return (
@@ -93,7 +95,7 @@ class TagTree extends Component { handleOnClick={handleTagClick} showReferenceCount={showReferenceCount} selectedTag={selectedTag} - showTagHover={showTagHover} + showTagHoverAdd={showTagHoverAdd} /> ))} {untaggedHandleClick ? ( @@ -112,12 +114,12 @@ TagTree.propTypes = { selectedTag: PropTypes.object, untaggedCount: PropTypes.number, untaggedHandleClick: PropTypes.func, - showTagHover: PropTypes.bool, + showTagHoverAdd: PropTypes.bool, }; TagTree.defaultProps = { showReferenceCount: false, handleTagClick: h.noop, - showTagHover: false, + showTagHoverAdd: false, }; export default TagTree; diff --git a/hawc/apps/lit/managers.py b/hawc/apps/lit/managers.py index 53ff3f8ef5..ec50549ac4 100644 --- a/hawc/apps/lit/managers.py +++ b/hawc/apps/lit/managers.py @@ -3,6 +3,7 @@ import pandas as pd from django.apps import apps +from django.contrib.postgres.aggregates import ArrayAgg from django.core.exceptions import ValidationError from django.core.validators import URLValidator from django.db import models @@ -351,6 +352,18 @@ def prune_tags(self, root_tag, pruned_tags, descendants: bool = False): ) return self.exclude(query).distinct("pk") + def user_tags(self, user_id: int) -> dict[int, list[int]]: + # Return a dictionary of reference_id: list[tag_ids] items for all references in a queryset + # TODO - update to annotate queryset with Django 4.1? + # https://docs.djangoproject.com/en/4.1/ref/contrib/postgres/expressions/#arraysubquery-expressions + UserReferenceTag = apps.get_model("lit", "UserReferenceTag") + user_qs = ( + UserReferenceTag.objects.filter(reference__in=self, user=user_id) + .annotate(tag_ids=ArrayAgg("tags__id")) + .values_list("reference_id", "tag_ids") + ) + return {reference_id: tag_ids for reference_id, tag_ids in user_qs} + class ReferenceManager(BaseManager): diff --git a/hawc/apps/lit/serializers.py b/hawc/apps/lit/serializers.py index 4438fc0c5d..a705aa788f 100644 --- a/hawc/apps/lit/serializers.py +++ b/hawc/apps/lit/serializers.py @@ -302,6 +302,7 @@ def to_representation(self, instance): ) ret["editReferenceUrl"] = reverse("lit:ref_edit", args=(instance.pk,)) ret["deleteReferenceUrl"] = reverse("lit:ref_delete", args=(instance.pk,)) + ret["tagHistoryUrl"] = reverse("lit:tag-history", args=(instance.pk,)) ret["identifiers"] = [ident.to_dict() for ident in instance.identifiers.all()] ret["searches"] = [search.to_dict() for search in instance.searches.all()] diff --git a/hawc/apps/lit/templates/lit/overview.html b/hawc/apps/lit/templates/lit/overview.html index a11e39c993..9239cf951b 100644 --- a/hawc/apps/lit/templates/lit/overview.html +++ b/hawc/apps/lit/templates/lit/overview.html @@ -107,7 +107,7 @@

Manually added references

window.app.startup("litStartup", function(lit){ let tagtree = new lit.TagTree(config.tags[0], config.assessment_id, null); - tagtree.render(document.getElementById('tags')) + tagtree.render(document.getElementById('tags'), {handleTagClick: (x) => {window.location.href = x.get_list_link()}}) }); const histo = document.getElementById("referenceYearHistogram"); diff --git a/hawc/apps/lit/views.py b/hawc/apps/lit/views.py index 186aee3313..98ddfe0d31 100644 --- a/hawc/apps/lit/views.py +++ b/hawc/apps/lit/views.py @@ -286,14 +286,19 @@ def get_context_data(self, **kwargs): return context def get_app_config(self, context) -> WebappConfig: + references = [ref.to_dict() for ref in context["object_list"]] + ref_tags = context["object_list"].user_tags(user_id=self.request.user.id) + for reference in references: + reference["user_tags"] = ref_tags.get(reference["pk"], []) return WebappConfig( app="litStartup", page="startupTagReferences", data=dict( + conflict_resolution=self.assessment.literature_settings.conflict_resolution, keywords=self.assessment.literature_settings.get_keyword_data(), instructions=self.assessment.literature_settings.screening_instructions, tags=models.ReferenceFilterTag.get_all_tags(self.assessment.id), - refs=[ref.to_dict() for ref in context["object_list"]], + refs=references, csrf=get_token(self.request), ), ) diff --git a/hawc/static/css/hawc.css b/hawc/static/css/hawc.css index b178e896cb..e9deef0bd8 100644 --- a/hawc/static/css/hawc.css +++ b/hawc/static/css/hawc.css @@ -312,7 +312,6 @@ div.smart-tag.active { .abstracts { font-size: 1.15rem; margin-top: 0.25rem; - max-height: 50vh; background-color: #f5f5f5; border-radius: 6px; } @@ -330,10 +329,16 @@ div.smart-tag.active { color: #ffffff; background-color: #162e51; border-radius: 10px; + border: 0.15rem dotted transparent; padding: 4px 7px 4px 7px; margin: 0 5px 7px 0; display: inline-block; } +.refUserTag { + color: #162e51; + background-color: #ffffff; + border: 0.15rem dotted #162e51; +} .referenceTag{ text-align: left; white-space: pre-wrap; @@ -349,7 +354,7 @@ div.smart-tag.active { border: 1px solid transparent; } -.nestedTag > div.tagName { +.nestedTag > div.tagHoverAdd { display: inline-block; position: relative; background: linear-gradient(0deg, #484848, #484848) no-repeat right bottom / 0 var(--bg-h); @@ -361,12 +366,12 @@ div.smart-tag.active { border: 3px solid transparent; } -.nestedTag > div.tagName:where(:hover, :focus-visible) { +.nestedTag > div.tagHoverAdd:where(:hover, :focus-visible) { background-size: 100% var(--bg-h); background-position-x: left; } -.nestedTag>div.tagName::before { +.nestedTag>div.tagHoverAdd::before { color: #484848; content: "\f067"; font-family: FontAwesome; @@ -384,15 +389,25 @@ div.smart-tag.active { transition: all 75ms ease-in 125ms; } -.nestedTag>div.tagName:hover::before { +.nestedTag>div.tagHoverAdd:hover::before { transform: scale(1, 1); } +.tagHoverSimple { + padding-left: 5px; + display: inline-block; + position: relative; + border: 2px solid transparent; + text-decoration: none; +} +.tagHoverSimple:hover, .tagSelected { + --bg-h: 2px; + background: linear-gradient(0deg, #484848, #484848) no-repeat right bottom / 0 var(--bg-h); + background-size: 100% var(--bg-h); + background-position-x: left; +} -.nestedTag.selected { - color: #0044cc; +.tagSelected { font-weight: bold; - border: 2px solid #ababab; - background-color: gold; } .reference { padding-left: 0.5rem;