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", {