From da628bc07773b8ca107cd8a5980bd9528c8986ec Mon Sep 17 00:00:00 2001 From: Guillaume Hivert Date: Wed, 22 Oct 2025 21:47:50 +0200 Subject: [PATCH 1/2] Add support for `latest` in query string --- src/hexdocs.gleam | 14 ++++++++++++ src/hexdocs/data/model.gleam | 32 +++++++++++++++++++++++++++- src/hexdocs/data/model/route.gleam | 5 +++++ src/hexdocs/data/model/version.gleam | 4 ++-- src/hexdocs/data/msg.gleam | 1 + src/hexdocs/effects.gleam | 23 ++++++++++++++++++++ src/hexdocs/view/search.gleam | 6 +++++- 7 files changed, 81 insertions(+), 4 deletions(-) diff --git a/src/hexdocs.gleam b/src/hexdocs.gleam index 8aa334f..b59934c 100644 --- a/src/hexdocs.gleam +++ b/src/hexdocs.gleam @@ -49,6 +49,8 @@ fn update(model: Model, msg: Msg) { msg.ApiReturnedPackages(response) -> api_returned_packages(model, response) msg.ApiReturnedTypesenseSearch(response) -> api_returned_typesense_search(model, response) + msg.ApiReturnedInitialLatestPackages(versions) -> + api_returned_initial_latest_packages(model, versions) msg.DocumentChangedLocation(location:) -> model.update_route(model, location) @@ -161,6 +163,18 @@ fn api_returned_typesense_search(model: Model, response: Loss(decode.Dynamic)) { |> result.unwrap(#(model, effect.none())) } +fn api_returned_initial_latest_packages( + model: Model, + versions: Loss(List(hexpm.Package)), +) -> #(Model, Effect(a)) { + case versions { + Error(_) -> #(model, toast.error("Server error. Retry later.")) + Ok(versions) -> + model.add_packages_versions(model, versions) + |> model.replace_search_packages + } +} + fn document_registered_event_listener(model: Model, unsubscriber: fn() -> Nil) { let dom_click_unsubscriber = Some(unsubscriber) Model(..model, dom_click_unsubscriber:) diff --git a/src/hexdocs/data/model.gleam b/src/hexdocs/data/model.gleam index c129e46..4315f36 100644 --- a/src/hexdocs/data/model.gleam +++ b/src/hexdocs/data/model.gleam @@ -229,14 +229,44 @@ pub fn update_route(model: Model, route: uri.Uri) { case string.is_empty(q) { True -> #(set_search_results(model, #(-1, [])), effect.none()) False -> { + let latest = list.filter(packages, fn(p) { p.1 == "latest" }) Model(..model, search_input: q, search_packages_filters: packages) - |> pair.new(effects.typesense_search(q, packages)) + |> pair.new({ + case latest { + [] -> effects.typesense_search(q, packages) + latest -> { + latest + |> list.map(pair.first) + |> effects.initial_latest_packages + } + } + }) } } } } } +pub fn replace_search_packages(model: Model) { + let model = + Model(..model, search_packages_filters: { + use #(package, version) <- list.map(model.search_packages_filters) + use <- bool.guard(when: version != "latest", return: #(package, version)) + case dict.get(model.packages_versions, package) { + Error(_) -> #(package, version) + Ok(versions) -> { + case versions.releases { + [] -> #(package, version) + [release, ..] -> #(package, release.version) + } + } + } + }) + route.Search(q: model.search_input, packages: model.search_packages_filters) + |> route.replace + |> pair.new(model, _) +} + pub fn select_autocomplete_option(model: Model, package: String) { case model.autocomplete, model.route { None, _ -> model diff --git a/src/hexdocs/data/model/route.gleam b/src/hexdocs/data/model/route.gleam index 1edd485..82b1f1d 100644 --- a/src/hexdocs/data/model/route.gleam +++ b/src/hexdocs/data/model/route.gleam @@ -47,6 +47,11 @@ pub fn push(route: Route) { modem.push(route.path, route.query, route.fragment) } +pub fn replace(route: Route) { + let route = to_uri(route) + modem.replace(route.path, route.query, route.fragment) +} + fn create_query( query: List(#(String, String)), packages: List(#(String, String)), diff --git a/src/hexdocs/data/model/version.gleam b/src/hexdocs/data/model/version.gleam index 5ae6ae6..ce344fa 100644 --- a/src/hexdocs/data/model/version.gleam +++ b/src/hexdocs/data/model/version.gleam @@ -1,11 +1,11 @@ import gleam/option.{type Option, None, Some} import gleam/regexp -const version_regexp = "^#([a-zA-Z_0-9]+)(:(([0-9]+|\\.){1,5}))?" +const version_regexp = "^#([a-zA-Z_0-9]+)(:((([0-9]+|\\.){1,5})|latest))?" pub fn match_package(word: String) -> Result(#(String, Option(String)), Nil) { let regexp = version_search() - case regexp.scan(regexp, word) { + case regexp.scan(regexp, word) |> echo { [regexp.Match(content: _, submatches:)] -> { case submatches { [Some(package), _, Some(version), ..] -> Ok(#(package, Some(version))) diff --git a/src/hexdocs/data/msg.gleam b/src/hexdocs/data/msg.gleam index b449147..e649b5f 100644 --- a/src/hexdocs/data/msg.gleam +++ b/src/hexdocs/data/msg.gleam @@ -9,6 +9,7 @@ pub type Msg { ApiReturnedPackages(Loss(String)) ApiReturnedTypesenseSearch(Loss(Dynamic)) ApiReturnedPackagesVersions(packages: Loss(List(hexpm.Package))) + ApiReturnedInitialLatestPackages(versions: Loss(List(hexpm.Package))) // Application messages. DocumentChangedLocation(location: uri.Uri) diff --git a/src/hexdocs/effects.gleam b/src/hexdocs/effects.gleam index 16ad4c4..1a569e7 100644 --- a/src/hexdocs/effects.gleam +++ b/src/hexdocs/effects.gleam @@ -2,6 +2,8 @@ import browser/document import gleam/function import gleam/http/response.{type Response} import gleam/javascript/promise +import gleam/list +import gleam/result import hexdocs/data/msg import hexdocs/loss.{type Loss} import hexdocs/services/hex @@ -24,6 +26,27 @@ pub fn package_versions(package: String) { dispatch(msg.ApiReturnedPackageVersions(response:)) } +pub fn initial_latest_packages(packages: List(String)) { + use dispatch <- effect.from() + use _ <- function.tap(Nil) + use response <- promise.map({ + promise.await_list({ + use package <- list.map(packages) + hex.package_versions(package) + }) + }) + let versions = case result.all(response) { + Error(error) -> Error(error) + Ok(response) -> { + case list.any(response, fn(r) { r.status != 200 }) { + True -> Error(loss.HttpError) + False -> Ok(list.map(response, fn(r) { r.body })) + } + } + } + dispatch(msg.ApiReturnedInitialLatestPackages(versions:)) +} + pub fn subscribe_blurred_search() { use dispatch <- effect.from() document.add_listener(fn() { dispatch(msg.UserBlurredSearch) }) diff --git a/src/hexdocs/view/search.gleam b/src/hexdocs/view/search.gleam index f060bef..fcb5da4 100644 --- a/src/hexdocs/view/search.gleam +++ b/src/hexdocs/view/search.gleam @@ -253,7 +253,11 @@ pub fn search(model: Model) { "self-stretch justify-start text-slate-700 dark:text-slate-400 text-sm font-normal leading-none", ), ], - [html.text(version)], + // You can add any loader you want here. + case version { + "latest" -> [html.text("Loading…")] + version -> [html.text(version)] + }, ), ], ), From cbee5247354a7a1b4065c5f1096a9fa8bb2b2409 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 23 Oct 2025 10:36:23 +0200 Subject: [PATCH 2/2] Update src/hexdocs/data/model/version.gleam --- src/hexdocs/data/model/version.gleam | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hexdocs/data/model/version.gleam b/src/hexdocs/data/model/version.gleam index ce344fa..7bbe953 100644 --- a/src/hexdocs/data/model/version.gleam +++ b/src/hexdocs/data/model/version.gleam @@ -5,7 +5,7 @@ const version_regexp = "^#([a-zA-Z_0-9]+)(:((([0-9]+|\\.){1,5})|latest))?" pub fn match_package(word: String) -> Result(#(String, Option(String)), Nil) { let regexp = version_search() - case regexp.scan(regexp, word) |> echo { + case regexp.scan(regexp, word) { [regexp.Match(content: _, submatches:)] -> { case submatches { [Some(package), _, Some(version), ..] -> Ok(#(package, Some(version)))