diff --git a/project/Coursier.scala b/project/Coursier.scala index 6c05d4303..8555dddf7 100644 --- a/project/Coursier.scala +++ b/project/Coursier.scala @@ -59,8 +59,10 @@ object CoursierBuild extends Build { organization := "com.github.alexarchambault", scalaVersion := "2.11.6", crossScalaVersions := Seq("2.10.5", "2.11.6"), - resolvers += "Scalaz Bintray Repo" at "http://dl.bintray.com/scalaz/releases", - resolvers += Resolver.sonatypeRepo("snapshots") + resolvers ++= Seq( + "Scalaz Bintray Repo" at "http://dl.bintray.com/scalaz/releases", + Resolver.sonatypeRepo("releases") + ) ) ++ publishingSettings private lazy val commonCoreSettings = commonSettings ++ Seq[Setting[_]]( @@ -124,7 +126,7 @@ object CoursierBuild extends Build { .settings( name := "coursier-cli", libraryDependencies ++= Seq( - "com.github.alexarchambault" %% "case-app" % "0.3.0-SNAPSHOT", + "com.github.alexarchambault" %% "case-app" % "0.3.0", "ch.qos.logback" % "logback-classic" % "1.1.3" ) ++ { if (scalaVersion.value startsWith "2.10.") diff --git a/web/src/main/scala/coursier/web/Backend.scala b/web/src/main/scala/coursier/web/Backend.scala index b0b4dc027..805cdbecc 100644 --- a/web/src/main/scala/coursier/web/Backend.scala +++ b/web/src/main/scala/coursier/web/Backend.scala @@ -14,17 +14,22 @@ import scala.concurrent.Future import scala.scalajs.js import js.Dynamic.{ global => g } -case class ResolutionOptions(followOptional: Boolean = false, - keepTest: Boolean = false) +case class ResolutionOptions( + followOptional: Boolean = false, + keepTest: Boolean = false +) -case class State(modules: Seq[Dependency], - repositories: Seq[MavenRepository], - options: ResolutionOptions, - resolutionOpt: Option[Resolution], - editModuleIdx: Int, - resolving: Boolean, - reverseTree: Boolean, - log: Seq[String]) +case class State( + modules: Seq[Dependency], + repositories: Seq[(String, MavenRepository)], + options: ResolutionOptions, + resolutionOpt: Option[Resolution], + editModuleIdx: Int, + editRepoIdx: Int, + resolving: Boolean, + reverseTree: Boolean, + log: Seq[String] +) class Backend($: BackendScope[Unit, State]) { def updateDepGraph(resolution: Resolution) = { @@ -39,12 +44,21 @@ class Backend($: BackendScope[Unit, State]) { nodes += name } + def repr(dep: Dependency) = + Seq( + dep.module.organization, + dep.module.name, + dep.scope.name + ).mkString(":") + for { - (dep, parents) <- resolution.reverseDependencies.toList - from = s"${dep.module.organization}:${dep.module.name}:${dep.scope.name}" + (dep, parents) <- resolution + .reverseDependencies + .toList + from = repr(dep) _ = addNode(from) parDep <- parents - to = s"${parDep.module.organization}:${parDep.module.name}:${parDep.scope.name}" + to = repr(parDep) _ = addNode(to) } { graph.addEdge(from, to) @@ -53,14 +67,21 @@ class Backend($: BackendScope[Unit, State]) { val layouter = js.Dynamic.newInstance(g.Graph.Layout.Spring)(graph) layouter.layout() - val width = jQuery("#dependencies").width() - val height = math.max(jQuery("#dependencies").height().asInstanceOf[Int], 400) + val width = jQuery("#dependencies") + .width() + val height = jQuery("#dependencies") + .height() + .asInstanceOf[Int] + .max(400) println(s"width: $width, height: $height") - jQuery("#depgraphcanvas").html("") //empty() + jQuery("#depgraphcanvas") + .html("") // empty() - val renderer = js.Dynamic.newInstance(g.Graph.Renderer.Raphael)("depgraphcanvas", graph, width, height) + val renderer = js.Dynamic.newInstance(g.Graph.Renderer.Raphael)( + "depgraphcanvas", graph, width, height + ) renderer.draw() println("Rendered canvas") } @@ -103,8 +124,14 @@ class Backend($: BackendScope[Unit, State]) { else Seq("nodes" -> js.Array(deps.map(tree): _*)) }: _*) - println(minDependencies.toList.map(tree).map(js.JSON.stringify(_))) - g.$(target).treeview(js.Dictionary("data" -> js.Array(minDependencies.toList.map(tree): _*))) + println( + minDependencies + .toList + .map(tree) + .map(js.JSON.stringify(_)) + ) + g.$(target) + .treeview(js.Dictionary("data" -> js.Array(minDependencies.toList.map(tree): _*))) } def resolve(action: => Unit = ()) = { @@ -130,21 +157,34 @@ class Backend($: BackendScope[Unit, State]) { def task = { val res = coursier.Resolution( s.modules.toSet, - filter = Some(dep => (s.options.followOptional || !dep.optional) && (s.options.keepTest || dep.scope != Scope.Test)) + filter = Some(dep => + (s.options.followOptional || !dep.optional) && + (s.options.keepTest || dep.scope != Scope.Test) + ) ) implicit val cachePolicy = CachePolicy.Default res .process - .run(s.repositories.map(r => r.copy(logger = Some(logger))), 100) + .run(s.repositories.map(item => item._2.copy(logger = Some(logger))), 100) } // For reasons that are unclear to me, not delaying this when using the runNow execution context // somehow discards the $.modState above. (Not a major problem as queue is used by default.) Future(task)(scala.scalajs.concurrent.JSExecutionContext.Implicits.queue).flatMap(_.runF).foreach { res: Resolution => - $.modState{ s => updateDepGraph(res); updateTree(res, "#deptree", reverse = s.reverseTree); s.copy(resolutionOpt = Some(res), resolving = false)} - g.$("#resResTab a:last").tab("show") + $.modState{ s => + updateDepGraph(res) + updateTree(res, "#deptree", reverse = s.reverseTree) + + s.copy( + resolutionOpt = Some(res), + resolving = false + ) + } + + g.$("#resResTab a:last") + .tab("show") } } def handleResolve(e: ReactEventI) = { @@ -173,14 +213,24 @@ class Backend($: BackendScope[Unit, State]) { def removeModule(idx: Int)(e: ReactEventI) = { e.preventDefault() - $.modState(s => s.copy(modules = s.modules.zipWithIndex.filter(_._2 != idx).map(_._1))) + $.modState(s => + s.copy( + modules = s.modules + .zipWithIndex + .filter(_._2 != idx) + .map(_._1) + ) + ) } def updateModule(moduleIdx: Int, update: (Dependency, String) => Dependency)(e: ReactEventI) = { if (moduleIdx >= 0) { $.modState{ state => val dep = state.modules(moduleIdx) - state.copy(modules = state.modules.updated(moduleIdx, update(dep, e.target.value))) + state.copy( + modules = state.modules + .updated(moduleIdx, update(dep, e.target.value)) + ) } } } @@ -190,20 +240,95 @@ class Backend($: BackendScope[Unit, State]) { $.modState{ state => val modules = state.modules :+ Dependency(Module("", ""), "") println(s"Modules:\n${modules.mkString("\n")}") - state.copy(modules = modules, editModuleIdx = modules.length - 1) + state.copy( + modules = modules, + editModuleIdx = modules.length - 1 + ) + } + } + + def editRepo(idx: Int)(e: ReactEventI) = { + e.preventDefault() + $.modState(_.copy(editRepoIdx = idx)) + } + + def removeRepo(idx: Int)(e: ReactEventI) = { + e.preventDefault() + $.modState(s => + s.copy( + repositories = s.repositories + .zipWithIndex + .filter(_._2 != idx) + .map(_._1) + ) + ) + } + + def moveRepo(idx: Int, up: Boolean)(e: ReactEventI) = { + e.preventDefault() + $.modState { s => + val idx0 = if (up) idx - 1 else idx + 1 + val n = s.repositories.length + + if (idx >= 0 && idx0 >= 0 && idx < n && idx0 < n) { + val a = s.repositories(idx) + val b = s.repositories(idx0) + + s.copy( + repositories = s.repositories + .updated(idx, b) + .updated(idx0, a) + ) + } else + s + } + } + + def updateRepo(repoIdx: Int, update: ((String, MavenRepository), String) => (String, MavenRepository))(e: ReactEventI) = { + if (repoIdx >= 0) { + $.modState{ state => + val repo = state.repositories(repoIdx) + state.copy( + repositories = state.repositories + .updated(repoIdx, update(repo, e.target.value)) + ) + } + } + } + + def addRepo(e: ReactEventI) = { + e.preventDefault() + $.modState{ state => + val repositories = state.repositories :+ ("" -> MavenRepository("")) + println(s"Repositories:\n${repositories.mkString("\n")}") + state.copy( + repositories = repositories, + editRepoIdx = repositories.length - 1 + ) } } def enablePopover(e: ReactEventI) = { - g.$("[data-toggle='popover']").popover() + g.$("[data-toggle='popover']") + .popover() } object options { def toggleOptional(e: ReactEventI) = { - $.modState(s => s.copy(options = s.options.copy(followOptional = !s.options.followOptional))) + $.modState(s => + s.copy( + options = s.options + .copy(followOptional = !s.options.followOptional) + ) + ) } def toggleTest(e: ReactEventI) = { - $.modState(s => s.copy(options = s.options.copy(keepTest = !s.options.keepTest))) + $.modState(s => + s.copy( + options = s.options + .copy(keepTest = !s.options.keepTest) + ) + ) } } } @@ -289,7 +414,16 @@ object App { ) ), <.tbody( - sortedDeps.map(dep => depItem(dep, res.projectCache.get(dep.moduleVersion).map(_._2.version).filter(_ != dep.version))) + sortedDeps.map(dep => + depItem( + dep, + res + .projectCache + .get(dep.moduleVersion) + .map(_._2.version) + .filter(_ != dep.version) + ) + ) ) ) } @@ -300,6 +434,8 @@ object App { def ok = apply("ok") def edit = apply("pencil") def remove = apply("remove") + def up = apply("arrow-up") + def down = apply("arrow-down") } val moduleEditModal = ReactComponentB[((Module, String), Int, Backend)]("EditModule") @@ -346,7 +482,7 @@ object App { } .build - def dependenciesTable(name: String) = ReactComponentB[(Seq[Dependency], Int, Backend)](name) + val modules = ReactComponentB[(Seq[Dependency], Int, Backend)]("Dependencies") .render{ P => val (deps, editModuleIdx, backend) = P @@ -373,7 +509,8 @@ object App { <.p( <.button(^.`type` := "button", ^.`class` := "btn btn-default customButton", ^.onClick ==> backend.addModule, - Attr("data-toggle") := "modal", Attr("data-target") := "#moduleEdit", + Attr("data-toggle") := "modal", + Attr("data-target") := "#moduleEdit", "Add" ) ), @@ -388,39 +525,131 @@ object App { ) ), <.tbody( - deps.zipWithIndex.map((depItem _).tupled) + deps.zipWithIndex + .map((depItem _).tupled) ) ), - moduleEditModal((deps.lift(editModuleIdx).fold((Module("", ""), ""))(_.moduleVersion), editModuleIdx, backend)) + moduleEditModal(( + deps + .lift(editModuleIdx) + .fold(Module("", "") -> "")(_.moduleVersion), + editModuleIdx, + backend + )) ) } .build - val modules = dependenciesTable("Dependencies") - - val repositories = ReactComponentB[Seq[MavenRepository]]("Repositories") - .render{ repos => - def repoItem(repo: MavenRepository) = - <.tr( - <.td( - <.a(^.href := repo.root, - repo.root + val repoEditModal = ReactComponentB[((String, MavenRepository), Int, Backend)]("EditRepo") + .render{ P => + val ((name, repo), repoIdx, backend) = P + <.div(^.`class` := "modal fade", ^.id := "repoEdit", ^.role := "dialog", ^.aria.labelledby := "repoEditTitle", + <.div(^.`class` := "modal-dialog", <.div(^.`class` := "modal-content", + <.div(^.`class` := "modal-header", + <.button(^.`type` := "button", ^.`class` := "close", Attr("data-dismiss") := "modal", ^.aria.label := "Close", + <.span(^.aria.hidden := "true", dangerouslySetInnerHtml("×")) + ), + <.h4(^.`class` := "modal-title", ^.id := "repoEditTitle", "Repository") + ), + <.div(^.`class` := "modal-body", + <.form( + <.div(^.`class` := "form-group", + <.label(^.`for` := "inputName", "Name"), + <.input(^.`class` := "form-control", ^.id := "inputName", ^.placeholder := "Name", + ^.onChange ==> backend.updateRepo(repoIdx, (item, value) => (value, item._2)), + ^.value := name + ) + ), + <.div(^.`class` := "form-group", + <.label(^.`for` := "inputVersion", "Root"), + <.input(^.`class` := "form-control", ^.id := "inputVersion", ^.placeholder := "Root", + ^.onChange ==> backend.updateRepo(repoIdx, (item, value) => (item._1, item._2.copy(root = value))), + ^.value := repo.root + ) + ), + <.div(^.`class` := "modal-footer", + <.button(^.`type` := "submit", ^.`class` := "btn btn-primary", Attr("data-dismiss") := "modal", "Done") + ) ) ) + )) + ) + } + .build + + val repositories = ReactComponentB[(Seq[(String, MavenRepository)], Int, Backend)]("Repositories") + .render{ P => + val (repos, editRepoIdx, backend) = P + + def repoItem(item: (String, MavenRepository), idx: Int, isLast: Boolean) = + <.tr( + <.td(item._1), + <.td(item._2.root), + <.td( + <.a(Attr("data-toggle") := "modal", Attr("data-target") := "#repoEdit", ^.`class` := "icon-action", + ^.onClick ==> backend.editRepo(idx), + icon.edit + ) + ), + <.td( + <.a(Attr("data-toggle") := "modal", Attr("data-target") := "#repoRemove", ^.`class` := "icon-action", + ^.onClick ==> backend.removeRepo(idx), + icon.remove + ) + ), + <.td( + if (idx > 0) + Seq(<.a(Attr("data-toggle") := "modal", Attr("data-target") := "#repoUp", ^.`class` := "icon-action", + ^.onClick ==> backend.moveRepo(idx, up = true), + icon.up + )) + else + Seq() + ), + <.td( + if (isLast) + Seq() + else + Seq(<.a(Attr("data-toggle") := "modal", Attr("data-target") := "#repoDown", ^.`class` := "icon-action", + ^.onClick ==> backend.moveRepo(idx, up = false), + icon.down + )) + ) ) - val sortedRepos = repos - .sortBy(repo => repo.root) - - <.table(^.`class` := "table", - <.thead( - <.tr( - <.th("Base URL") + <.div( + <.p( + <.button(^.`type` := "button", ^.`class` := "btn btn-default customButton", + ^.onClick ==> backend.addRepo, + Attr("data-toggle") := "modal", + Attr("data-target") := "#repoEdit", + "Add" ) ), - <.tbody( - sortedRepos.map(repoItem) - ) + <.table(^.`class` := "table", + <.thead( + <.tr( + <.th("Name"), + <.th("Root"), + <.th(""), + <.th(""), + <.th(""), + <.th("") + ) + ), + <.tbody( + repos.init.zipWithIndex + .map(t => repoItem(t._1, t._2, isLast = false)) ++ + repos.lastOption.map(repoItem(_, repos.length - 1, isLast = true)) + ) + ), + repoEditModal(( + repos + .lift(editRepoIdx) + .getOrElse("" -> MavenRepository("")), + editRepoIdx, + backend + )) ) } .build @@ -471,7 +700,17 @@ object App { } .build - val initialState = State(Nil, Seq(Repository.mavenCentral), ResolutionOptions(), None, -1, resolving = false, reverseTree = false, log = Nil) + val initialState = State( + Nil, + Seq("central" -> Repository.mavenCentral), + ResolutionOptions(), + None, + -1, + -1, + resolving = false, + reverseTree = false, + log = Nil + ) val app = ReactComponentB[Unit]("Coursier") .initialState(initialState) @@ -501,7 +740,7 @@ object App { modules((S.modules, S.editModuleIdx, B)) ), <.div(^.role := "tabpanel", ^.`class` := "tab-pane", ^.id := "repositories", - repositories(S.repositories) + repositories((S.repositories, S.editRepoIdx, B)) ), <.div(^.role := "tabpanel", ^.`class` := "tab-pane", ^.id := "options", options((S.options, B))