UsdaImporter.scala 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  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. Foundation, Survey, SRLegacy
  54. ).map(_.toString), pageSize = Some(SEARCH_PAGE_SIZE))().map {
  55. case SearchResult(criteria, n, cur, tot, baseList) =>
  56. Val(Some(baseList)) +:
  57. (cur + 1 to tot).map({ (c) => lazyFutureSignal(
  58. USDAController.postFoodsSearch()(
  59. criteria.copy(pageNumber = Some(c))
  60. ).map(_.foods),
  61. Nil
  62. ) })
  63. },
  64. inSignal = queryPage.map(_.q).withDefault("")
  65. )
  66. val searchResults = searchBar.result
  67. val pageSizeSel = Select(Val((10 to SEARCH_PAGE_SIZE by 10)))
  68. val numPages = pageSizeSel.value.combineWithFn(searchResults) {
  69. case (_, None) => 0
  70. case (size, Some(results)) => results.length * SEARCH_PAGE_SIZE / size
  71. }
  72. val pageSel = PageSelect(numPages)
  73. val currentSearchPageNum = pageSel.page.combineWithFn(pageSizeSel.value) {
  74. case (pageNum, pageSize) => pageNum * pageSize / SEARCH_PAGE_SIZE
  75. }
  76. val currentSearchPageOffset = pageSel.page.combineWithFn(pageSizeSel.value) {
  77. case (pageNum, pageSize) => pageNum % (SEARCH_PAGE_SIZE / pageSize)
  78. }
  79. val currentSearchPage = searchResults.combineWithFn(currentSearchPageNum) {
  80. case (Some(Nil), _) => Val(Nil)
  81. case (None, _) => Val(Nil)
  82. // [2023-09-26]@tflucke TODO: implement loading UI instead of default
  83. case (Some(res), pageNum) => res(pageNum).withDefault(Nil)
  84. }.flatMap(identity)
  85. // TODO: Prefetch next page
  86. div(
  87. h2("USDA Importer"),
  88. div(cls := "form-group",
  89. label(forId := "search", "Search: "),
  90. searchBar.render
  91. ),
  92. searchBar.searchTerm --> { (str: Option[String]) =>
  93. val oldQ = View.router.currentPageSignal.now().asInstanceOf[ViewPage].q
  94. if (oldQ != str)
  95. View.router.pushState(ViewPage(q = str))
  96. },
  97. PaginatedTable[SearchResultFood](Seq(
  98. ("", 1, { (x) => button(cls := "btn btn-light",
  99. onClick --> {(e: Event) =>
  100. val editor = USDAEditor(USDANodeNoId.fromSearchResult(x), true)
  101. Overlay.confirm(editor.render) { () =>
  102. import com.weEat.Main.headers
  103. FoodController.add()(editor.getUSDANode())
  104. }
  105. },
  106. "Add"
  107. )}),
  108. ("ID", 1, { (x) => span(x.fdcId.toString)}),
  109. ("Name", 3, { (x) => span(x.description)}),
  110. ("Desc", 4, { (x) => span(x.additionalDescriptions)}),
  111. ("Brand", 3, { (x) => span(x.brandOwner)})
  112. ),
  113. currentSearchPage,
  114. currentSearchPageOffset,
  115. pageSizeSel.value
  116. ).render,
  117. (pageSel.render).amendThis(
  118. (elm) => cls := s"${elm.ref.className} col-3 float-left"
  119. ),
  120. (pageSizeSel.render).amendThis(
  121. (elm) => cls := s"${elm.ref.className} col-1 float-right"
  122. )
  123. )
  124. }
  125. }