UsdaImporter.scala 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. package com.weEat.view
  2. import com.raquo.laminar.api.L._
  3. import io.laminext.syntax.core._
  4. import com.raquo.waypoint._
  5. import play.api.libs.json.{JsValue,Json}
  6. import com.weEat.controllers.{FoodController,USDAController}
  7. import com.weEat.modules._
  8. import com.weEat.shared.models.USDANodeNoId
  9. import gov.usda.nal.fdc.models._
  10. import gov.usda.nal.fdc.models.DataType._
  11. import org.scalajs.dom.Event
  12. import scala.concurrent.Future
  13. import scala.util.Success
  14. object UsdaImporter extends View[Option[String]] {
  15. import com.weEat.Main.headers
  16. implicit val ctx = com.weEat.shared.ctx
  17. val navName = Some("Usda Import")
  18. val tag = "importer"
  19. override val permissions = Set("admin")
  20. case class ViewPage(val q: Option[String] = None) extends P {
  21. val title = "USDA Import" + q.map((q) => s" - $q").getOrElse("")
  22. def jsonValue = Json.toJson(q)
  23. }
  24. def parseJson(jsVal: JsValue) = ViewPage(jsVal.asOpt[String])
  25. def route = Route.onlyQuery(
  26. encode = (page: ViewPage) => page.q,
  27. decode = (q: Option[String]) => ViewPage(q = q),
  28. pattern = (root / tag / endOfSegments) ? param[String]("q").?
  29. )
  30. def defaultPage = ViewPage()
  31. import com.raquo.airstream.custom.CustomSource._
  32. def lazyFutureSignal[T](fut: => Future[T]) =
  33. Signal.fromCustomSource(
  34. Success(fut),
  35. (_: SetCurrentValue[Future[T]], _: GetCurrentValue[Future[T]], _, _) => (),
  36. (_) => ()
  37. ).flatMap { (fut) =>
  38. Signal.fromFuture(fut)
  39. }
  40. def lazyFutureSignal[T](fut: => Future[T], default: => T) =
  41. Signal.fromCustomSource(
  42. Success(fut),
  43. (_: SetCurrentValue[Future[T]], _: GetCurrentValue[Future[T]], _, _) => (),
  44. (_) => ()
  45. ).flatMap { (fut) =>
  46. Signal.fromFuture(fut)
  47. }
  48. private val SEARCH_PAGE_SIZE = 40.asInstanceOf[Short]
  49. def content(queryPage: Signal[ViewPage]) = {
  50. val searchBar: SearchBar[Seq[Signal[Option[Seq[SearchResultFood]]]]] =
  51. SearchBar((term) =>
  52. USDAController.getFoodsSearch(term, Seq(
  53. // Branded,
  54. Foundation, SRLegacy
  55. ).map(_.toString), pageSize = Some(SEARCH_PAGE_SIZE))().map {
  56. case SearchResult(criteria, n, cur, tot, baseList) =>
  57. Val(Some(baseList)) +:
  58. (cur + 1 to tot).map({ (c) => lazyFutureSignal(
  59. USDAController.postFoodsSearch()(
  60. criteria.copy(pageNumber = Some(c))
  61. ).map(_.foods),
  62. Nil
  63. ) })
  64. },
  65. inSignal = queryPage.map(_.q).withDefault("")
  66. )
  67. val searchResults = searchBar.result
  68. val pageSizeSel = Select(Val((10 to SEARCH_PAGE_SIZE by 10)))
  69. val numPages = pageSizeSel.value.combineWithFn(searchResults) {
  70. case (_, None) => 0
  71. case (size, Some(results)) => results.length * SEARCH_PAGE_SIZE / size
  72. }
  73. val pageSel = PageSelect(numPages)
  74. val currentSearchPageNum = pageSel.page.combineWithFn(pageSizeSel.value) {
  75. case (pageNum, pageSize) => pageNum * pageSize / SEARCH_PAGE_SIZE
  76. }
  77. val currentSearchPageOffset = pageSel.page.combineWithFn(pageSizeSel.value) {
  78. case (pageNum, pageSize) => pageNum % (SEARCH_PAGE_SIZE / pageSize)
  79. }
  80. val currentSearchPage = searchResults.combineWithFn(currentSearchPageNum) {
  81. case (Some(Nil), _) => Val(Nil)
  82. case (None, _) => Val(Nil)
  83. // [2023-09-26]@tflucke TODO: implement loading UI instead of default
  84. case (Some(res), pageNum) => res(pageNum).withDefault(Nil)
  85. }.flatMap(identity)
  86. // TODO: Prefetch next page
  87. div(
  88. h2("USDA Importer"),
  89. div(cls := "form-group",
  90. label(forId := "search", "Search: "),
  91. searchBar.render
  92. ),
  93. searchBar.searchTerm --> { (str: Option[String]) =>
  94. val oldQ = View.router.currentPageSignal.now().asInstanceOf[ViewPage].q
  95. if (oldQ != str)
  96. View.router.pushState(ViewPage(q = str))
  97. },
  98. PaginatedTable[SearchResultFood](Seq(
  99. ("", 1, { (x) => button(cls := "btn btn-light",
  100. onClick --> {(e: Event) =>
  101. val editor = USDAEditor(USDANodeNoId.fromSearchResult(x), true)
  102. Overlay.confirm(editor.render) { () =>
  103. import com.weEat.Main.headers
  104. FoodController.add()(editor.getUSDANode())
  105. }
  106. },
  107. "Add"
  108. )}),
  109. ("ID", 1, { (x) => span(x.fdcId.toString)}),
  110. ("Name", 3, { (x) => span(x.description)}),
  111. ("Desc", 4, { (x) => span(x.additionalDescriptions)}),
  112. ("Brand", 3, { (x) => span(x.brandOwner)})
  113. ),
  114. currentSearchPage,
  115. currentSearchPageOffset,
  116. pageSizeSel.value
  117. ).render,
  118. (pageSel.render).amendThis(
  119. (elm) => cls := s"${elm.ref.className} col-3 float-left"
  120. ),
  121. (pageSizeSel.render).amendThis(
  122. (elm) => cls := s"${elm.ref.className} col-1 float-right"
  123. )
  124. )
  125. }
  126. }