Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 42 additions & 11 deletions R/detail_dataset.R
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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("<div>", as.character(citation_html), "</div>")))
copy_icon <- load_svg_icon(
"copy",
style = "width:13px;height:13px;vertical-align:-0.1em;flex-shrink:0;"
Expand Down Expand Up @@ -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/<doi>`
#' - VegBank legacy -> `https://identifiers.org/vegbank:<accession>`
#' - 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)) {
Expand Down
16 changes: 10 additions & 6 deletions R/detail_helpers.R
Original file line number Diff line number Diff line change
Expand Up @@ -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/<vb_code>` 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/<vb_code>`; 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;"
Expand All @@ -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)
}
Expand Down
2 changes: 2 additions & 0 deletions inst/shiny/www/vegbank_styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
2 changes: 1 addition & 1 deletion tests/testthat/test_detail_concept.R
Original file line number Diff line number Diff line change
Expand Up @@ -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", {
Expand Down
56 changes: 44 additions & 12 deletions tests/testthat/test_detail_dataset.R
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 <- '<script>alert("xss")</script>'
text <- build_dataset_citation_text(
text <- build_dataset_citation_html(
ds_xss,
author_name = "Kyle Palmquist",
start_year = "2017"
Expand Down Expand Up @@ -87,15 +87,38 @@ 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)
html <- htmltools::renderTags(details$dataset_header(shinysession = mock_session))$html

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", {
Expand Down Expand Up @@ -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"
Expand All @@ -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"
Expand All @@ -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))
})
2 changes: 1 addition & 1 deletion tests/testthat/test_detail_helpers.R
Original file line number Diff line number Diff line change
Expand Up @@ -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", {
Expand Down
2 changes: 1 addition & 1 deletion tests/testthat/test_detail_plot.R
Original file line number Diff line number Diff line change
Expand Up @@ -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", {
Expand Down