Selaa lähdekoodia

Views are now dynamically loaded from a single entry point.

The server serves a view loader which then gets populated by a View object based
on the query string parameters.  This is a variant of what I already had, but
without the redundant server compoent.  New views are automatically avaliable by
adding it to the list.

I would also like to generate the navigation dynamically from the view list.

Refactored OAuth client functionality into a seperate object.

Cleaned up imports.
Thomas Flucke 5 vuotta sitten
vanhempi
commit
abceef4651

+ 17 - 182
client/src/main/scala/com/weEat/Main.scala

@@ -1,201 +1,36 @@
 package com.weEat
 
 import com.tflucke.webroutes.Headers
-import org.scalajs.dom.document
-import org.scalajs.dom.raw.{Element,HTMLInputElement,MouseEvent,HTMLDivElement}
-import scala.scalajs.js
-import scala.scalajs.js.annotation.JSExportTopLevel
-import scala.scalajs.js.timers
-import scala.util.{Try,Success,Failure}
+import com.weEat.view.View
+import org.querki.jquery.{JQueryStatic => $}
+import org.scalajs.dom.{document,window}
+import org.scalajs.dom.raw.HTMLInputElement
 import scala.concurrent.ExecutionContext.Implicits.global
-import scala.concurrent.Future
-import com.weEat.shared.models._
-import com.weEat.controllers.UserController
-import org.querki.jquery.{JQueryEventObject,JQuery,JQueryXHR,JQueryStatic => $}
-import com.tflucke.webroutes.{HTTPException,TimeoutException}
-import play.api.libs.json.Json
-import com.weEat.util.SessionStorage
 
 object Main {
+  implicit def headers = Headers(Seq(
+    csrfHeader,
+    OAuthManager.authHeader
+  ).flatten.toMap)
+
   /* Get the CSRF token embedded in the HTML page.  This token will implicitly
    * be passed to Rest RPC.
    */
-  implicit def headers = Headers(Seq(csrfHeader, authHeader).flatten.toMap)
-
-  @JSExportTopLevel(name="asyncCount")
-  var asyncCount = 0
-
-  def track[T](future: Future[T]): Future[T] = {
-    asyncCount += 1
-    future andThen {
-      case _=> asyncCount -= 1
-    }
-  }
-
   val csrfSelector = "input[name=\"csrfToken\"]"
   def csrfHeader = Option(document.querySelector(csrfSelector)).map({ x =>
     ("Csrf-Token" -> x.asInstanceOf[HTMLInputElement].value)
   })
 
-  def authHeader = SessionStorage.get("access-token").map({ token =>
-    ("Authorization" -> token)
-  })
-
   def main(args: Array[String]): Unit = {
-    refreshToken
-    $("#btn-login").click(promptLogin _)
-    $("#btn-signup").click(promptSignup _)
-    $("#btn-logout").click(logout _)
-  }
-
-  def promptLogin() = {
-    $("body").append(overlayWindow("/assets/views/login.html", login))
-  }
-
-  def promptSignup() = {
-    $("body").append(overlayWindow("/assets/views/register.html", signup))
-  }
-
-  def refreshToken: Unit = {
-    SessionStorage.remove("access-token")
-    SessionStorage.get("username").map({user =>
-      SessionStorage.get("refresh-token").map({refresh =>
-        track(UserController.accessToken()(RefreshRequest(user, refresh))
-          .map(loginComplete(user)).recover({
-            case _ => SessionStorage.remove("username").remove("refresh-token")
-          }))
-      })
-    }).flatten
-  }
+    import org.scalajs.dom.experimental.URLSearchParams
 
-  def loginComplete(user: String)(auth: UserAuthorization) = {
-    SessionStorage.set("access-token",
-      "%s %s".format(auth.tokenType, auth.accessToken)
-    )
-    SessionStorage.set("username", user)
-    SessionStorage.set("refresh-token", auth.refreshToken)
-    timers.setTimeout(auth.expiresIn)(refreshToken)
-    $("#login-btns").hide
-    $("#logout-btns").show
-    //track()
-  }
-
-  def signup(div: JQuery): Future[Any] = {
-    import java.util.InputMismatchException
-
-    val email = div.find("#email").value.toString
-    val password = div.find("#password").value.toString
-    track((if (!password.equals(div.find("#password2").value.toString))
-      Future.failed(new InputMismatchException("Passwords do not match."))
-    else
-      UserController.registerUser()(UserRegistration(
-        div.find("#fname").value.toString,
-        div.find("#lname").value.toString,
-        email,
-        password
-      )) map(loginComplete(email))) andThen({
-        case Failure(e: InputMismatchException) => showError(div, e.getMessage)
-        case Failure(e: HTTPException) => showError(div, e.responseText)
-        case Failure(e: TimeoutException) => showError(div, e.getMessage)
-      }))
-  }
-
-  @js.native
-  @js.annotation.JSGlobal("btoa")
-  def base64Encode(str: String): String = js.native
-
-  def showError(div: JQuery, msg: String) = div.find(".alert-danger").html(
-    s"<strong>Error:</strong> $msg"
-  ).show
-
-  def parseString[T](implicit reader: play.api.libs.json.Reads[T]) = {
-    str: String => Json.parse(str).as[T]
-  }
-
-  def login(div: JQuery): Future[Any] = {
-    val email = div.find("#email").value.toString
-    implicit var headers = Main.headers +
-      ("Authorization" -> ("Basic " + base64Encode(
-        "%s:%s".format(email, div.find("#password").value)
-      )))
-    track(UserController.accessToken()(PasswordRequest()).andThen({
-      case Success(auth) => loginComplete(email)(auth)
-      case Failure(error: HTTPException) => Try(
-        error.responseObject(parseString[GrantError])
-      ) match {
-        case Success(gError) => showError(div, gError.errorDescription)
-        case Failure(_) => showError(div, error.getMessage)
-      }
-      case Failure(error: TimeoutException) => showError(div, error.getMessage)
-    }))
-  }
-
-  def logout: Future[Unit] = {
-    track(SessionStorage.get("username").map({user =>
-      SessionStorage.get("refresh-token").map({ refresh: String =>
-        UserController.revokeAccessToken()(RefreshRequest(user, refresh))
-      })
-    }).flatten.getOrElse(Future.failed(
-      new IllegalStateException("No login information.")
-    )).map({ _ =>
-      SessionStorage.remove("username").remove("refresh-token")
-      document.location.reload(false)
-    }))
-  }
-
-  def shadeWindow(cancelFn: Option[(() => Unit)]) = {
-    val shadeDiv = $("<div>").css(js.Dictionary[js.Any](
-      "z-index" -> 99,
-      "background-color" -> "rgba(0, 0, 0, 0.5)",
-      "position" -> "fixed",
-      "top" -> 0,
-      "bottom" -> 0,
-      "left" -> 0,
-      "right" -> 0
-    ))
-    shadeDiv.click((event: JQueryEventObject) => {
-      if (event.target == shadeDiv(0))
-      {
-        cancelFn match {
-          case Some(fn) => fn()
-          case None =>
-        }
-        $(event.target).detach
-      }
-    })
-  }
+    OAuthManager.refreshToken
+    $("#btn-login").click(OAuthManager.promptLogin _)
+    $("#btn-signup").click(OAuthManager.promptSignup _)
+    $("#btn-logout").click(OAuthManager.logout _)
 
-  def overlayWindow[T](
-    contentUrl: String,
-    submitFn: (JQuery) => Future[T],
-    cancelFn: Option[() => Unit] = None
-  ) = {
-    val promptDiv = $("<div>").css(js.Dictionary[js.Any](
-      "margin" -> "auto",
-      "padding" -> "1em",
-      "background-color" -> "#ffffff",
-      "position" -> "relative",
-      "border-radius" -> "1em",
-      "top" -> "50%",
-      "transform" -> "translateY(-50%)"
-    )).addClass("w-50")
-    val shadeDiv = shadeWindow(cancelFn).append(promptDiv)
-    promptDiv.load(contentUrl, "",
-      (elm: Element, resp: String, status: String, xhr: JQueryXHR) => {
-        $(elm).find("*[data-cb='cancel']").click((event: JQueryEventObject) => {
-          cancelFn.map({fn => fn()})
-          shadeDiv.detach
-        })
-        val div =  $(elm).find("*[data-cb='success']").click(
-          (event: JQueryEventObject) => {
-            submitFn(promptDiv) onComplete {
-              case Success(_) => shadeDiv.detach
-              case Failure(err) => System.err.println(err)
-            }
-          }
-        )
-      }
-    )
-    shadeDiv.append(promptDiv)
+    View.fromTag(
+      new URLSearchParams(window.location.search).get("t")
+    ).present
   }
 }

+ 167 - 0
client/src/main/scala/com/weEat/OAuthManager.scala

@@ -0,0 +1,167 @@
+package com.weEat
+
+import com.tflucke.webroutes.{HTTPException,TimeoutException}
+import com.weEat.controllers.UserController
+import com.weEat.shared.models._
+import com.weEat.util.SessionStorage
+import org.querki.jquery.{JQueryEventObject,JQuery,JQueryXHR,JQueryStatic => $}
+import org.scalajs.dom.document
+import org.scalajs.dom.raw.Element
+import play.api.libs.json.Json
+import scala.concurrent.ExecutionContext.Implicits.global
+import scala.concurrent.Future
+import scala.scalajs.js
+import scala.scalajs.js.timers
+import scala.util.{Try,Success,Failure}
+
+object OAuthManager {
+  def authHeader = SessionStorage.get("access-token").map({ token =>
+    ("Authorization" -> token)
+  })
+
+  def promptLogin() =
+    $("body").append(overlayWindow("/assets/views/login.html", login))
+
+  def promptSignup() =
+    $("body").append(overlayWindow("/assets/views/register.html", signup))
+
+  def refreshToken: Unit = {
+    SessionStorage.remove("access-token")
+    SessionStorage.get("username").map({user =>
+      SessionStorage.get("refresh-token").map({refresh =>
+        UserController.accessToken()(RefreshRequest(user, refresh))
+          .map(loginComplete(user)).recover({
+            case _ => SessionStorage.remove("username").remove("refresh-token")
+          })
+      })
+    }).flatten
+  }
+
+  def loginComplete(user: String)(auth: UserAuthorization) = {
+    SessionStorage.set("access-token",
+      "%s %s".format(auth.tokenType, auth.accessToken)
+    )
+    SessionStorage.set("username", user)
+    SessionStorage.set("refresh-token", auth.refreshToken)
+    timers.setTimeout(auth.expiresIn)(refreshToken)
+    $("#login-btns").hide
+    $("#logout-btns").show
+  }
+
+  def signup(div: JQuery): Future[Any] = {
+    import java.util.InputMismatchException
+
+    val email = div.find("#email").value.toString
+    val password = div.find("#password").value.toString
+    (if (!password.equals(div.find("#password2").value.toString))
+      Future.failed(new InputMismatchException("Passwords do not match."))
+    else
+      UserController.registerUser()(UserRegistration(
+        div.find("#fname").value.toString,
+        div.find("#lname").value.toString,
+        email,
+        password
+      )) map(loginComplete(email))) andThen({
+        case Failure(e: InputMismatchException) => showError(div, e.getMessage)
+        case Failure(e: HTTPException) => showError(div, e.responseText)
+        case Failure(e: TimeoutException) => showError(div, e.getMessage)
+      })
+  }
+
+  @js.native
+  @js.annotation.JSGlobal("btoa")
+  def base64Encode(str: String): String = js.native
+
+  def showError(div: JQuery, msg: String) = div.find(".alert-danger").html(
+    s"<strong>Error:</strong> $msg"
+  ).show
+
+  def parseString[T](implicit reader: play.api.libs.json.Reads[T]) = {
+    str: String => Json.parse(str).as[T]
+  }
+
+  def login(div: JQuery): Future[Any] = {
+    val email = div.find("#email").value.toString
+    implicit var headers = Main.headers +
+      ("Authorization" -> ("Basic " + base64Encode(
+        "%s:%s".format(email, div.find("#password").value)
+      )))
+    UserController.accessToken()(PasswordRequest()).andThen({
+      case Success(auth) => loginComplete(email)(auth)
+      case Failure(error: HTTPException) => Try(
+        error.responseObject(parseString[GrantError])
+      ) match {
+        case Success(gError) => showError(div, gError.errorDescription)
+        case Failure(_) => showError(div, error.getMessage)
+      }
+      case Failure(error: TimeoutException) => showError(div, error.getMessage)
+    })
+  }
+
+  def logout: Future[Unit] = SessionStorage.get("username").map({user =>
+    SessionStorage.get("refresh-token").map({ refresh: String =>
+      UserController.revokeAccessToken()(RefreshRequest(user, refresh))
+    })
+  }).flatten.getOrElse(Future.failed(
+    new IllegalStateException("No login information.")
+  )).map({ _ =>
+    SessionStorage.remove("username").remove("refresh-token")
+    document.location.reload(false)
+  })
+
+  def shadeWindow(cancelFn: Option[(() => Unit)]) = {
+    val shadeDiv = $("<div>").css(js.Dictionary[js.Any](
+      "z-index" -> 99,
+      "background-color" -> "rgba(0, 0, 0, 0.5)",
+      "position" -> "fixed",
+      "top" -> 0,
+      "bottom" -> 0,
+      "left" -> 0,
+      "right" -> 0
+    ))
+    shadeDiv.click((event: JQueryEventObject) => {
+      if (event.target == shadeDiv(0))
+      {
+        cancelFn match {
+          case Some(fn) => fn()
+          case None =>
+        }
+        $(event.target).detach
+      }
+    })
+  }
+
+  def overlayWindow[T](
+    contentUrl: String,
+    submitFn: (JQuery) => Future[T],
+    cancelFn: Option[() => Unit] = None
+  ) = {
+    val promptDiv = $("<div>").css(js.Dictionary[js.Any](
+      "margin" -> "auto",
+      "padding" -> "1em",
+      "background-color" -> "#ffffff",
+      "position" -> "relative",
+      "border-radius" -> "1em",
+      "top" -> "50%",
+      "transform" -> "translateY(-50%)"
+    )).addClass("w-50")
+    val shadeDiv = shadeWindow(cancelFn).append(promptDiv)
+    promptDiv.load(contentUrl, "",
+      (elm: Element, resp: String, status: String, xhr: JQueryXHR) => {
+        $(elm).find("*[data-cb='cancel']").click((event: JQueryEventObject) => {
+          cancelFn.map({fn => fn()})
+          shadeDiv.detach
+        })
+        val div =  $(elm).find("*[data-cb='success']").click(
+          (event: JQueryEventObject) => {
+            submitFn(promptDiv) onComplete {
+              case Success(_) => shadeDiv.detach
+              case Failure(err) => System.err.println(err)
+            }
+          }
+        )
+      }
+    )
+    shadeDiv.append(promptDiv)
+  }
+}

+ 1 - 1
client/src/main/scala/com/weEat/modules/Module.scala

@@ -1,5 +1,5 @@
 package com.weEat.modules
 
 trait Module {
-  def render: scala.xml.Node
+  val render: scala.xml.Node
 }

+ 4 - 3
client/src/main/scala/com/weEat/modules/Overlay.scala

@@ -1,9 +1,8 @@
 package com.weEat.modules
 
-import org.scalajs.dom.document
-import org.scalajs.dom.raw.{MouseEvent,Element,HTMLDivElement}
-import scala.concurrent.{Future,ExecutionContext}
 import mhtml.{mount,Rx}
+import org.scalajs.dom.document
+import org.scalajs.dom.raw.{MouseEvent,HTMLDivElement}
 import scala.xml.Node
 
 case class Overlay(
@@ -42,6 +41,8 @@ case class Overlay(
 }
 
 object Overlay {
+  import scala.concurrent.{Future,ExecutionContext}
+
   implicit val ec = scala.concurrent.ExecutionContext.global
 
   def confirm(

+ 2 - 2
client/src/main/scala/com/weEat/modules/PaginatedTable.scala

@@ -1,7 +1,7 @@
 package com.weEat.modules
 
-import scala.xml.Node
 import mhtml.Rx
+import scala.xml.Node
 
 case class PaginatedTable[T](
   structure: Seq[(String, Short, (T) => Rx[Node])],
@@ -9,7 +9,7 @@ case class PaginatedTable[T](
   page: Rx[Int] = Rx(0),
   pageSize: Rx[Int] = Rx(10)
 ) extends Module {
-  def render = {
+  val render = {
     <table class="table table-striped table-hover">
       <thead>
         <tr>

+ 8 - 10
client/src/main/scala/com/weEat/modules/SearchBar.scala

@@ -1,18 +1,16 @@
 package com.weEat.modules
 
-import org.scalajs.dom.raw.Event
-import scala.concurrent.duration._
-import scala.concurrent.{ExecutionContext,Future,Promise}
-import scala.scalajs.js.timers.{clearTimeout, setTimeout}
-import mhtml.Rx
-import mhtml.future.syntax._
-import scala.util.Try
 import cats.implicits._
-import mhtml.implicits.cats._
 import cats.Traverse._
-import scala.xml.{Node,Elem,UnprefixedAttribute}
 import com.weEat.modules._
 import com.weEat.util.MHtmlHelpers._
+import mhtml.Rx
+import mhtml.future.syntax._
+import mhtml.implicits.cats._
+import scala.concurrent.duration._
+import scala.concurrent.{ExecutionContext,Future,Promise}
+import scala.scalajs.js.timers.{clearTimeout, setTimeout}
+import scala.util.Try
 
 case class SearchBar[T](
   searchFn: (String) => Future[T],
@@ -28,7 +26,7 @@ case class SearchBar[T](
     }
     promise.future
   }
-  implicit class RxRxFlattener[T](outer: Rx[Rx[T]]) {
+  implicit class RxFlattener[T](outer: Rx[Rx[T]]) {
     def flatten = outer.flatMap(identity)
   }
 

+ 1 - 2
client/src/main/scala/com/weEat/modules/Select.scala

@@ -1,8 +1,7 @@
 package com.weEat.modules
 
-import org.scalajs.dom.raw.Event
-import mhtml.Rx
 import com.weEat.util.MHtmlHelpers._
+import mhtml.Rx
 
 case class Select[T](values: Rx[Seq[T]], defaul: Option[T] = None) extends Module {
   private val (_select, _value) =

+ 1 - 1
client/src/main/scala/com/weEat/util/MHtmlHelpers.scala

@@ -1,8 +1,8 @@
 package com.weEat.util
 
-import scala.xml._
 import mhtml.{Var,Rx}
 import org.scalajs.dom.raw._
+import scala.xml._
 
 object MHtmlHelpers {
   implicit class RxHelpers[T](rx: Rx[T]) {

+ 9 - 13
client/src/main/scala/com/weEat/views/UsdaImporter.scala

@@ -1,25 +1,24 @@
 package com.weEat.view
 
-import scala.scalajs.js.annotation._
+import com.weEat.controllers.{FoodController,USDAController}
+import com.weEat.modules._
+import com.weEat.shared.models.UnitType._
+import com.weEat.shared.models.USDANode
+import com.weEat.util.MHtmlHelpers._
 import gov.usda.nal.fdc.models._
 import gov.usda.nal.fdc.models.DataType._
-import com.weEat.controllers.{FoodController,USDAController}
+import mhtml.Rx
+import mhtml.future.syntax._
 import org.scalajs.dom.raw.Event
-import org.scalajs.dom.html.Element
-import com.weEat.shared.models.UnitType._
-import com.weEat.shared.models.{Count,MeasureUnit,USDANode}
 import scala.util.Success
-import mhtml.{mount,Rx}
-import mhtml.future.syntax._
-import com.weEat.util.MHtmlHelpers._
-import com.weEat.modules._
 
-@JSExportTopLevel("UsdaImporter")
 object UsdaImporter extends View {
   import com.weEat.Main.headers
 
   implicit val ctx = scala.concurrent.ExecutionContext.global
 
+  val tag = "importer"
+
   val title = "USDA Import"
 
   def content = {
@@ -83,7 +82,4 @@ object UsdaImporter extends View {
     { (pageSizeSel.render).addClass("col-1 float-right") }
     </div>
   }
-
-  @JSExport
-  def render(parent: Element) = mhtml.mount(parent, content)
 }

+ 22 - 0
client/src/main/scala/com/weEat/views/View.scala

@@ -1,6 +1,28 @@
 package com.weEat.view
 
+import org.scalajs.dom.document
+
 trait View {
+  def tag: String
   def title: String
   def content: scala.xml.Node
+
+  def present = {
+    document.title =
+      document.title.split(View.titleSeperator)(0) + View.titleSeperator + title
+    mhtml.mount(View._contentDiv, content)
+  }
+}
+
+object View {
+  private val _contentDiv = document.getElementById("content")
+
+  val home = UsdaImporter
+  val titleSeperator = " - "
+  val views = Seq(
+    UsdaImporter
+  )
+
+  def fromTagOpt(tag: String) = views.find(_.tag == tag)
+  def fromTag(tag: String) = fromTagOpt(tag).getOrElse(home)
 }

+ 8 - 11
server/app/com/weEat/controllers/FoodController.scala

@@ -1,20 +1,17 @@
 package com.weEat.controllers
 
-import javax.inject.{Inject,Singleton}
-import play.api._
-import play.api.mvc._
-import play.api.libs.json._
-import scala.util.{Try,Success,Failure}
-import com.tflucke.webroutes.HTTPException
-import scala.concurrent.{ExecutionContext,Future}
 import com.weEat.models.{FoodNode => FoodNodeCollection}
-import com.weEat.shared.models.{UnitType,USDANode}
-import com.weEat.shared.models.UnitType._
 import com.weEat.services.MongoDBService
-import org.mongodb.scala.model.Filters._
-import org.mongodb.scala.result.UpdateResult
 import com.weEat.shared.models._
+import com.weEat.shared.models.UnitType._
+import javax.inject.{Inject,Singleton}
+import play.api._
+import play.api.libs.json._
+import play.api.mvc._
 import org.bson.types.ObjectId
+import org.mongodb.scala.model.Filters._
+import scala.concurrent.{ExecutionContext,Future}
+import scala.util.{Success,Failure}
 
 @Singleton
 class FoodController @Inject()(

+ 2 - 13
server/app/com/weEat/controllers/ViewController.scala

@@ -15,18 +15,7 @@ class ViewController @Inject()(
   val webJarsUtil: org.webjars.play.WebJarsUtil
 ) extends BaseController {
 
-  /**
-   * Create an Action to render an HTML page.
-   *
-   * The configuration in the `routes` file means that this method
-   * will be called when the application receives a `GET` request with
-   * a path of `/`.
-   */
-  def index() = Action { implicit request: Request[AnyContent] =>
-    Ok(views.html.index())
-  }
-
-  def usdaImport() = Action { implicit request: Request[AnyContent] =>
-    Ok(views.html.usdaImport())
+  def loader() = Action { implicit request: Request[AnyContent] =>
+    Ok(views.html.viewLoader())
   }
 }

+ 0 - 4
server/app/views/main.scala.html

@@ -60,10 +60,6 @@
     routes.Assets.versioned(_).toString,
     name => getClass.getResource(s"/public/$name") != null
   )
-  <script>
-    // Selenium can access vars, but not lets
-    var selAsyncCount = asyncCount;
-  </script>
   @lastly
 </body>
 </html>

+ 0 - 6
server/app/views/recipieEdit.scala.html

@@ -1,6 +0,0 @@
-@()(implicit webJarsUtil: org.webjars.play.WebJarsUtil, request: RequestHeader)
-
-@main("Home") {
-<h1>Hello!</h1>
-<h2>Welcome to my recipe book!</h2>
-}

+ 0 - 8
server/app/views/usdaImport.scala.html

@@ -1,8 +0,0 @@
-@()(implicit webJarsUtil: org.webjars.play.WebJarsUtil, request: RequestHeader)
-
-@main("USDA Import") {
-  <div id="importPane"></div>
-}
-
-<script>UsdaImporter.render(document.getElementById("importPane"))</script>
-

+ 1 - 1
server/app/views/viewLoader.scala.html

@@ -1,4 +1,4 @@
-@(implicit webJarsUtil: org.webjars.play.WebJarsUtil, request: RequestHeader)
+@()(implicit webJarsUtil: org.webjars.play.WebJarsUtil, request: RequestHeader)
 <!DOCTYPE html>
 <html>
 <head>

+ 3 - 3
server/conf/routes

@@ -3,9 +3,9 @@
 # https://www.playframework.com/documentation/latest/ScalaRouting
 # ~~~~
 
-GET   /               com.weEat.controllers.ViewController.index()
+GET   /               @controllers.Default.redirect(to = "/view")
 
-GET   /views/import   com.weEat.controllers.ViewController.usdaImport()
+GET   /view           com.weEat.controllers.ViewController.loader()
 
 # Shared Route
 # body: com.weEat.shared.models.UserRegistration
@@ -72,4 +72,4 @@ GET   /fdc/food/:id   com.weEat.controllers.USDAController.getFood(id: Long, fmt
 GET   /assets/*file   controllers.Assets.versioned(path="/public", file: Asset)
 
 # Forward Webjar requests to the webjar routes
-->      /webjars                            webjars.Routes
+->      /webjars                            webjars.Routes