From 99eaa4668dd99e13e759b138739a14d80f378dad Mon Sep 17 00:00:00 2001 From: qqmyers Date: Tue, 11 Aug 2026 16:40:37 -0400 Subject: [PATCH 1/8] autocomplete for large cvv --- .../edu/harvard/iq/dataverse/DatasetPage.java | 74 ++++++++ .../dataverse/search/AdvancedSearchPage.java | 80 +++++++++ src/main/java/propertyFiles/Bundle.properties | 6 + src/main/webapp/metadataFragment.xhtml | 162 ++++++++++++++---- src/main/webapp/resources/css/structure.css | 12 ++ src/main/webapp/search/advanced.xhtml | 112 ++++++++++-- 6 files changed, 398 insertions(+), 48 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java index 2c78873fa71..499776fe620 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java @@ -7041,4 +7041,78 @@ public void setRequestedCSL(String requestedCSL) { public void validateEmbargoReason(FacesContext context, UIComponent component, Object value) { FileUtil.validateEmbargoReason(context, component, value, removeEmbargo); } + + private final Map autocompleteLimits = new HashMap<>(); + private final Map autocompleteLastQueries = new HashMap<>(); + private final Set slowModeFieldTypeIds = new HashSet<>(); + + public List completeControlledVocabularyValue(String query) { + UIComponent component = UIComponent.getCurrentComponent(FacesContext.getCurrentInstance()); + DatasetField dsf = (DatasetField) component.getAttributes().get("dsf"); + if (dsf == null || dsf.getDatasetFieldType() == null || dsf.getDatasetFieldType().getControlledVocabularyValues() == null) { + return Collections.emptyList(); + } + + String clientId = component.getClientId(); + String lastQuery = autocompleteLastQueries.get(clientId); + Integer limit = autocompleteLimits.get(clientId); + + if (limit == null || !StringUtils.equals(StringUtils.trimToEmpty(query), StringUtils.trimToEmpty(lastQuery))) { + limit = 100; + autocompleteLimits.put(clientId, limit); + } + autocompleteLastQueries.put(clientId, query != null ? query : ""); + + List results = new ArrayList<>(); + String queryLower = query.toLowerCase(); + String mdLangCode = null; + if (dsf.getDatasetVersion() != null && dsf.getDatasetVersion().getDataset() != null) { + mdLangCode = dsf.getDatasetVersion().getDataset().getMetadataLanguage(); + } + + for (ControlledVocabularyValue cvv : dsf.getDatasetFieldType().getControlledVocabularyValues()) { + if (cvv.getLocaleStrValue(mdLangCode).toLowerCase().contains(queryLower)) { + results.add(cvv); + } + if (results.size() >= limit + 1) { + break; + } + } + return results; + } + + public void onMoreText() { + Map params = FacesContext.getCurrentInstance().getExternalContext().getRequestParameterMap(); + String clientId = params.get("autocompleteClientId"); + String query = params.get("autocompleteQuery"); + String widgetVar = params.get("autocompleteWidgetVar"); + if (clientId != null) { + Integer limit = autocompleteLimits.get(clientId); + if (limit == null) { + limit = 100; + } + autocompleteLimits.put(clientId, limit + 100); + autocompleteLastQueries.put(clientId, query != null ? query : ""); + + if (widgetVar != null) { + PrimeFaces.current().ajax().addCallbackParam("widgetVar", widgetVar); + PrimeFaces.current().ajax().addCallbackParam("query", query); + PrimeFaces.current().ajax().addCallbackParam("scrollPos", params.get("autocompleteScrollPos")); + } + } + } + + public Map getAutocompleteLimits() { + return autocompleteLimits; + } + + public boolean isSlowMode(Long fieldTypeId) { + return fieldTypeId != null && slowModeFieldTypeIds.contains(fieldTypeId); + } + + public void switchToSlowMode(Long fieldTypeId) { + if (fieldTypeId != null) { + slowModeFieldTypeIds.add(fieldTypeId); + } + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/search/AdvancedSearchPage.java b/src/main/java/edu/harvard/iq/dataverse/search/AdvancedSearchPage.java index bc92959a5ac..8b6d7048726 100644 --- a/src/main/java/edu/harvard/iq/dataverse/search/AdvancedSearchPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/search/AdvancedSearchPage.java @@ -15,17 +15,22 @@ import java.net.URLEncoder; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.logging.Logger; import jakarta.ejb.EJB; +import jakarta.faces.component.UIComponent; +import jakarta.faces.context.FacesContext; import jakarta.faces.view.ViewScoped; import jakarta.inject.Inject; import jakarta.inject.Named; import jakarta.json.JsonObject; import org.apache.commons.lang3.StringUtils; +import org.primefaces.PrimeFaces; @ViewScoped @Named("AdvancedSearchPage") @@ -265,6 +270,81 @@ public Collection getDvFieldSubjectValues() { return subjectType.getControlledVocabularyValues(); } + public DatasetFieldType getSubjectDatasetFieldType() { + return datasetFieldService.findByName(DatasetFieldConstant.subject); + } + + private final Map autocompleteLimits = new HashMap<>(); + private final Map autocompleteLastQueries = new HashMap<>(); + private final Set slowModeFieldTypeIds = new java.util.HashSet<>(); + + public List completeControlledVocabularyValue(String query) { + UIComponent component = UIComponent.getCurrentComponent(FacesContext.getCurrentInstance()); + DatasetFieldType dsft = (DatasetFieldType) component.getAttributes().get("dsft"); + if (dsft == null || dsft.getControlledVocabularyValues() == null || query == null) { + return Collections.emptyList(); + } + + String clientId = component.getClientId(); + String lastQuery = autocompleteLastQueries.get(clientId); + Integer limit = autocompleteLimits.get(clientId); + + if (limit == null || !StringUtils.equals(StringUtils.trimToEmpty(query), StringUtils.trimToEmpty(lastQuery))) { + limit = 100; + autocompleteLimits.put(clientId, limit); + } + autocompleteLastQueries.put(clientId, query != null ? query : ""); + + List results = new ArrayList<>(); + String queryLower = query.toLowerCase(); + + for (ControlledVocabularyValue cvv : dsft.getControlledVocabularyValues()) { + String localeLabel = cvv.getLocaleStrValue(); + if (localeLabel != null && localeLabel.toLowerCase().contains(queryLower)) { + results.add(cvv); + } + if (results.size() >= limit + 1) { + break; + } + } + return results; + } + + public void onMoreText() { + Map params = FacesContext.getCurrentInstance().getExternalContext().getRequestParameterMap(); + String clientId = params.get("autocompleteClientId"); + String query = params.get("autocompleteQuery"); + String widgetVar = params.get("autocompleteWidgetVar"); + if (clientId != null) { + Integer limit = autocompleteLimits.get(clientId); + if (limit == null) { + limit = 100; + } + autocompleteLimits.put(clientId, limit + 100); + autocompleteLastQueries.put(clientId, query != null ? query : ""); + + if (widgetVar != null) { + PrimeFaces.current().ajax().addCallbackParam("widgetVar", widgetVar); + PrimeFaces.current().ajax().addCallbackParam("query", query); + PrimeFaces.current().ajax().addCallbackParam("scrollPos", params.get("autocompleteScrollPos")); + } + } + } + + public Map getAutocompleteLimits() { + return autocompleteLimits; + } + + public boolean isSlowMode(Long fieldTypeId) { + return fieldTypeId != null && slowModeFieldTypeIds.contains(fieldTypeId); + } + + public void switchToSlowMode(Long fieldTypeId) { + if (fieldTypeId != null) { + slowModeFieldTypeIds.add(fieldTypeId); + } + } + public String getDsPublicationDate() { return dsPublicationDate; } diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index 0825816048d..182aa1865e1 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -3298,3 +3298,9 @@ abstractPaginatedCommand.errors.negativePaginationParam=The {0} parameter cannot #UpdateDatasetLicenseCommand.java updateDatasetLicenseCommand.errors.licenseNotActive=License {0} cannot be set because it is not active. updateDatasetLicenseCommand.errors.customTermsOfUseNotProvided=Terms of use text should be provided in custom terms. + +# Controlled Vocabulary Autocomplete +controlledvocabulary.autocomplete.watermark=Start typing to search... +controlledvocabulary.autocomplete.moreText=Only the first 100 matches shown. Type to filter or click to load more. +controlledvocabulary.showAll=See all entries (slow) +controlledvocabulary.showAll.tooltip=Load the full list. This may be slow for large vocabularies. diff --git a/src/main/webapp/metadataFragment.xhtml b/src/main/webapp/metadataFragment.xhtml index 37246ddf40f..ac392997131 100755 --- a/src/main/webapp/metadataFragment.xhtml +++ b/src/main/webapp/metadataFragment.xhtml @@ -299,23 +299,45 @@
- - - - - - - - + + + + + + + + + + + + + + + +
#{dsf.validationMessage}
@@ -380,30 +402,47 @@
- - - - - - - - + + + + + + + + + + + + + +
#{subdsf.validationMessage} @@ -437,6 +476,57 @@
+ +