diff --git a/R/detail_dataset.R b/R/detail_dataset.R index cab1fd9..114e836 100644 --- a/R/detail_dataset.R +++ b/R/detail_dataset.R @@ -38,9 +38,11 @@ build_dataset_details_view <- function(result) { ) ) + permalink_url <- dataset_accession_url(as.character(ds$accession_code %|||% "")) header_rows <- add_permalink_button_to_last_row( header_rows, - ds$ds_code + ds$ds_code, + copy_url = permalink_url ) htmltools::div(htmltools::tagList(header_rows)) @@ -103,8 +105,8 @@ build_dataset_details_view <- function(result) { }), dataset_citation = shiny::renderUI({ - citation_html <- build_dataset_citation_text(ds, author_name, start_year) - citation_text <- xml2::xml_text(xml2::read_html(as.character(citation_html))) + citation_html <- build_dataset_citation_html(ds, author_name, start_year) + citation_text <- xml2::xml_text(xml2::read_html(paste0("
", as.character(citation_html), "
"))) copy_icon <- load_svg_icon( "copy", style = "width:13px;height:13px;vertical-align:-0.1em;flex-shrink:0;" @@ -151,26 +153,55 @@ parse_dataset_author_label <- function(owner_label) { } -#' Build Dataset Citation Text (HTML) +#' Extract Clean DOI from Accession Code +#' +#' Returns the bare DOI (without any "doi:" prefix) when `raw_accession` +#' matches the DOI pattern, or NULL otherwise. +#' @noRd +extract_clean_doi <- function(raw_accession) { + if (grepl("^(doi:)?10\\.\\d{4,9}/", raw_accession)) { + sub("^doi:", "", raw_accession) + } else { + NULL + } +} + +#' Resolve Dataset Accession Code to a Canonical URL +#' +#' Returns the best persistent URL for a dataset accession code: +#' - DOI accession -> `https://doi.org/` +#' - VegBank legacy -> `https://identifiers.org/vegbank:` +#' - Unrecognised -> NULL (caller falls back to https://vegbank.org/cite) +#' @noRd +dataset_accession_url <- function(raw_accession) { + clean_doi <- extract_clean_doi(raw_accession) + if (!is.null(clean_doi)) { + return(paste0("https://doi.org/", clean_doi)) + } + if (grepl("^VB\\.ds\\.\\d+\\.", raw_accession)) { + return(paste0("https://identifiers.org/vegbank:", raw_accession)) + } + NULL +} + +#' Build Dataset Citation HTML #' #' Returns the HTML-formatted citation string for a VegBank dataset. #' @noRd -build_dataset_citation_text <- function(ds, author_name, start_year) { +build_dataset_citation_html <- function(ds, author_name, start_year) { safe_author_html <- htmltools::htmlEscape(as.character(author_name %|||% "Unknown Author")) safe_name_html <- htmltools::htmlEscape(as.character(ds$name %|||% "Unnamed Dataset")) raw_accession <- as.character(ds$accession_code %|||% "Unspecified") - is_doi <- grepl("^10\\.\\d{4,9}/", raw_accession) + url <- dataset_accession_url(raw_accession) + clean_doi <- extract_clean_doi(raw_accession) is_vegbank <- grepl("^VB\\.ds\\.\\d+\\.", raw_accession) - if (is_doi) { - display <- paste0("doi:", raw_accession) - url <- paste0("https://doi.org/", raw_accession) + if (!is.null(clean_doi)) { + display <- paste0("doi:", clean_doi) } else if (is_vegbank) { display <- paste0("vegbank:", raw_accession) - url <- paste0("https://identifiers.org/vegbank:", raw_accession) } else { display <- raw_accession - url <- NULL } if (!is.null(url)) { diff --git a/R/detail_helpers.R b/R/detail_helpers.R index cdeab34..df0dad2 100644 --- a/R/detail_helpers.R +++ b/R/detail_helpers.R @@ -418,20 +418,23 @@ create_detail_link <- function(input_id, code_value, display_text) { #' Create Copy permalink Link Button #' -#' Renders a minimal inline button that copies `vegbank.org/cite/` to -#' the clipboard. The click behavior is implemented in `vegbank_app.js`. +#' Renders a minimal inline button that copies a URL to the clipboard. +#' By default the URL is `https://vegbank.org/cite/`; pass +#' `copy_url` to override (e.g., a DOI URL for datasets with a DOI accession). +#' The click behavior is implemented in `vegbank_app.js`. #' #' @param vb_code VegBank code (e.g., "ob.1234", "cc.567", "ds.987") #' @param label Button label text (defaults to "Copy permalink") +#' @param copy_url Optional URL string to copy instead of the default cite URL #' @return An htmltools button tag, or NULL when vb_code is missing #' @noRd -create_permalink_button <- function(vb_code, label = "Copy permalink") { +create_permalink_button <- function(vb_code, label = "Copy permalink", copy_url = NULL) { if (is.null(vb_code) || length(vb_code) == 0 || is.na(vb_code) || !nzchar(trimws(as.character(vb_code)))) { return(NULL) } code <- trimws(as.character(vb_code)[1]) - copy_text <- paste0("vegbank.org/cite/", code) + copy_text <- if (!is.null(copy_url)) copy_url else paste0("https://vegbank.org/cite/", code) copy_icon <- load_svg_icon( "copy", style = "width:13px;height:13px;vertical-align:-0.1em;flex-shrink:0;" @@ -457,16 +460,17 @@ create_permalink_button <- function(vb_code, label = "Copy permalink") { #' #' @param rows List of htmltools tag elements (for header rows) #' @param vb_code VegBank code used for the permalink /cite URL +#' @param copy_url Optional URL string passed through to `create_permalink_button` #' @return A list of header row tags with the last row wrapped in #' `div.vb-copy-inline-row` when a copy button can be created #' @noRd -add_permalink_button_to_last_row <- function(rows, vb_code) { +add_permalink_button_to_last_row <- function(rows, vb_code, copy_url = NULL) { rows <- Filter(Negate(is.null), rows) if (length(rows) == 0) { return(rows) } - copy_button <- create_permalink_button(vb_code) + copy_button <- create_permalink_button(vb_code, copy_url = copy_url) if (is.null(copy_button)) { return(rows) } diff --git a/inst/shiny/www/vegbank_styles.css b/inst/shiny/www/vegbank_styles.css index 6189828..1258403 100644 --- a/inst/shiny/www/vegbank_styles.css +++ b/inst/shiny/www/vegbank_styles.css @@ -415,12 +415,14 @@ table.dataTable tbody tr.selected-entity:hover, position: fixed; top: var(--navbar-height); height: calc(100vh - var(--navbar-height)); + height: calc(100dvh - var(--navbar-height)); overflow-y: auto; background: #fff; border-left: 1px solid rgba(40, 70, 94, 0.15); box-shadow: -4px 0 20px rgba(0, 0, 0, 0.12), -2px 0 8px rgba(0, 0, 0, 0.08); z-index: 1050; padding: 0 20px 0; + padding: 0 20px env(safe-area-inset-bottom); transition: right 0.35s cubic-bezier(0.4, 0, 0.2, 1); } diff --git a/tests/testthat/test_detail_concept.R b/tests/testthat/test_detail_concept.R index 6aca435..582ac9a 100644 --- a/tests/testthat/test_detail_concept.R +++ b/tests/testthat/test_detail_concept.R @@ -57,7 +57,7 @@ test_that("build_comm_concept_details_view header includes copy permalink button expect_true(grepl("vb-copy-permalink", html, fixed = TRUE)) expect_true(grepl("Copy permalink", html, fixed = TRUE)) - expect_true(grepl("vegbank.org/cite/cc.47882", html, fixed = TRUE)) + expect_true(grepl("https://vegbank.org/cite/cc.47882", html, fixed = TRUE)) }) test_that("build_plant_concept_details_view handles valid data", { diff --git a/tests/testthat/test_detail_dataset.R b/tests/testthat/test_detail_dataset.R index 0b16e4e..93e4e7a 100644 --- a/tests/testthat/test_detail_dataset.R +++ b/tests/testthat/test_detail_dataset.R @@ -19,10 +19,10 @@ test_that("parse_dataset_author_label handles NULL / NA / empty", { expect_equal(parse_dataset_author_label(" "), "Unknown Author") }) -# ==== build_dataset_citation_text() ==== +# ==== build_dataset_citation_html() ==== -test_that("build_dataset_citation_text builds correct citation — start only", { - text <- build_dataset_citation_text( +test_that("build_dataset_citation_html builds correct citation — start only", { + text <- build_dataset_citation_html( mock_dataset_ds201120, author_name = "Kyle Palmquist", start_year = "2017" @@ -35,10 +35,10 @@ test_that("build_dataset_citation_text builds correct citation — start only", expect_true(grepl("VB.ds.201120.DWLFOT", text, fixed = TRUE)) }) -test_that("build_dataset_citation_text sanitizes dataset name", { +test_that("build_dataset_citation_html sanitizes dataset name", { ds_xss <- mock_dataset_ds201120 ds_xss$name <- '' - text <- build_dataset_citation_text( + text <- build_dataset_citation_html( ds_xss, author_name = "Kyle Palmquist", start_year = "2017" @@ -87,7 +87,7 @@ test_that("build_dataset_details_view header contains ds_code and name", { expect_true(grepl("unnamed dataset", html2, fixed = TRUE)) }) -test_that("build_dataset_details_view header includes copy permalink button", { +test_that("build_dataset_details_view header includes copy permalink button — VegBank accession uses identifiers.org", { mock_session <- shiny::MockShinySession$new() details <- build_dataset_details_view(mock_dataset_ds201120) @@ -95,7 +95,30 @@ test_that("build_dataset_details_view header includes copy permalink button", { expect_true(grepl("vb-copy-permalink", html, fixed = TRUE)) expect_true(grepl("Copy permalink", html, fixed = TRUE)) - expect_true(grepl("vegbank.org/cite/ds.201120", html, fixed = TRUE)) + expect_true(grepl("https://identifiers.org/vegbank:VB.ds.201120.DWLFOT", html, fixed = TRUE)) + expect_false(grepl("vegbank.org/cite", html, fixed = TRUE)) +}) + +test_that("build_dataset_details_view header uses identifiers.org permalink for unnamed VegBank dataset", { + mock_session <- shiny::MockShinySession$new() + + details <- build_dataset_details_view(mock_dataset_ds201398) + html <- htmltools::renderTags(details$dataset_header(shinysession = mock_session))$html + + expect_true(grepl("vb-copy-permalink", html, fixed = TRUE)) + expect_true(grepl("https://identifiers.org/vegbank:VB.ds.201398.UNNAMEDDATASET", html, fixed = TRUE)) + expect_false(grepl("vegbank.org/cite", html, fixed = TRUE)) +}) + +test_that("build_dataset_details_view header uses doi.org permalink for DOI accession", { + mock_session <- shiny::MockShinySession$new() + + details <- build_dataset_details_view(mock_dataset_ds201910) + html <- htmltools::renderTags(details$dataset_header(shinysession = mock_session))$html + + expect_true(grepl("vb-copy-permalink", html, fixed = TRUE)) + expect_true(grepl("https://doi.org/10.5072/FK26D61D4V", html, fixed = TRUE)) + expect_false(grepl("vegbank.org/cite", html, fixed = TRUE)) }) test_that("build_dataset_details_view details card contains accession, author, plot count link", { @@ -168,10 +191,10 @@ test_that("build_dataset_details_view details plot count link has correct ds_cod expect_true(grepl("obs-count-link", html, fixed = TRUE)) }) -# ==== build_dataset_citation_text() accession/DOI link rendering ==== +# ==== build_dataset_citation_html() accession/DOI link rendering ==== -test_that("build_dataset_citation_text renders VegBank accession as correct link and label", { - tag <- build_dataset_citation_text( +test_that("build_dataset_citation_html renders VegBank accession as correct link and label", { + tag <- build_dataset_citation_html( mock_dataset_ds201120, author_name = "Kyle Palmquist", start_year = "2017" @@ -181,8 +204,8 @@ test_that("build_dataset_citation_text renders VegBank accession as correct link expect_true(grepl("href=\"https://identifiers.org/vegbank:VB.ds.201120.DWLFOT\"", html, fixed = TRUE)) }) -test_that("build_dataset_citation_text renders DOI as correct link and label", { - tag <- build_dataset_citation_text( +test_that("build_dataset_citation_html renders DOI as correct link and label", { + tag <- build_dataset_citation_html( mock_dataset_ds201910, author_name = "Rushirah Nenuji", start_year = "2026" @@ -191,3 +214,12 @@ test_that("build_dataset_citation_text renders DOI as correct link and label", { expect_true(grepl(">doi:10.5072/FK26D61D4V<", html, fixed = TRUE)) expect_true(grepl("href=\"https://doi.org/10.5072/FK26D61D4V\"", html, fixed = TRUE)) }) + +test_that("build_dataset_citation_html renders doi:-prefixed accession as link", { + ds <- mock_dataset_ds201910 + ds$accession_code <- "doi:10.82902/J17P4J" + tag <- build_dataset_citation_html(ds, author_name = "Matthew Jones", start_year = "2026") + html <- as.character(tag) + expect_true(grepl(">doi:10.82902/J17P4J<", html, fixed = TRUE)) + expect_true(grepl("href=\"https://doi.org/10.82902/J17P4J\"", html, fixed = TRUE)) +}) diff --git a/tests/testthat/test_detail_helpers.R b/tests/testthat/test_detail_helpers.R index 4575030..82827fa 100644 --- a/tests/testthat/test_detail_helpers.R +++ b/tests/testthat/test_detail_helpers.R @@ -276,7 +276,7 @@ test_that("create_permalink_button builds a copy control with cite URL", { expect_true(grepl("vb-copy-permalink", html, fixed = TRUE)) expect_true(grepl("Copy permalink", html, fixed = TRUE)) expect_false(grepl("Copy permalink link", html, fixed = TRUE)) - expect_true(grepl("vegbank.org/cite/ob.2948", html, fixed = TRUE)) + expect_true(grepl("https://vegbank.org/cite/ob.2948", html, fixed = TRUE)) }) test_that("create_permalink_button returns NULL for missing vb_code", { diff --git a/tests/testthat/test_detail_plot.R b/tests/testthat/test_detail_plot.R index b44a650..5e4767e 100644 --- a/tests/testthat/test_detail_plot.R +++ b/tests/testthat/test_detail_plot.R @@ -275,7 +275,7 @@ test_that("build_plot_obs_details_view header includes copy permalink button", { expect_true(grepl("vb-copy-permalink", html, fixed = TRUE)) expect_true(grepl("Copy permalink", html, fixed = TRUE)) - expect_true(grepl("vegbank.org/cite/ob.2948", html, fixed = TRUE)) + expect_true(grepl("https://vegbank.org/cite/ob.2948", html, fixed = TRUE)) }) test_that("normalize_plot_obs_result only uses first row of multi-row input", {