mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-22 10:28:27 +00:00
First version of new ui based on tailwind
This drops fomantic-ui as css toolkit and introduces tailwindcss. With tailwind there are no predefined components, but it's very easy to create those. So customizing the look&feel is much simpler, most of the time no additional css is needed. This requires a complete rewrite of the markup + styles. Luckily all logic can be kept as is. The now old ui is not removed, it is still available by using a request header `Docspell-Ui` with a value of `1` for the old ui and `2` for the new ui. Another addition is "dev mode", where docspell serves assets with a no-cache header, to disable browser caching. This makes developing a lot easier.
This commit is contained in:
@ -5,7 +5,7 @@ import java.nio.file.{Files, Paths}
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.common.{Banner, Pools, ThreadFactories}
|
||||
import docspell.common._
|
||||
|
||||
import org.log4s._
|
||||
|
||||
@ -57,6 +57,10 @@ object Main extends IOApp {
|
||||
} yield Pools(cec, bec, blocker, rec)
|
||||
|
||||
logger.info(s"\n${banner.render("***>")}")
|
||||
if (EnvMode.current.isDev) {
|
||||
logger.warn(">>>>> Docspell is running in DEV mode! <<<<<")
|
||||
}
|
||||
|
||||
pools.use(p =>
|
||||
RestServer
|
||||
.stream[IO](cfg, p)
|
||||
|
@ -5,7 +5,8 @@ import cats.implicits._
|
||||
import fs2.Stream
|
||||
|
||||
import docspell.backend.auth.AuthToken
|
||||
import docspell.common.Pools
|
||||
import docspell.common._
|
||||
import docspell.restserver.http4s.EnvMiddleware
|
||||
import docspell.restserver.routes._
|
||||
import docspell.restserver.webapp._
|
||||
|
||||
@ -39,9 +40,9 @@ object RestServer {
|
||||
adminRoutes(cfg, restApp)
|
||||
},
|
||||
"/api/doc" -> templates.doc,
|
||||
"/app/assets" -> WebjarRoutes.appRoutes[F](pools.blocker),
|
||||
"/app" -> templates.app,
|
||||
"/sw.js" -> templates.serviceWorker,
|
||||
"/app/assets" -> EnvMiddleware(WebjarRoutes.appRoutes[F](pools.blocker)),
|
||||
"/app" -> EnvMiddleware(templates.app),
|
||||
"/sw.js" -> EnvMiddleware(templates.serviceWorker),
|
||||
"/" -> redirectTo("/app")
|
||||
).orNotFound
|
||||
|
||||
|
@ -0,0 +1,13 @@
|
||||
package docspell.restserver.http4s
|
||||
|
||||
import cats.Functor
|
||||
|
||||
import docspell.common._
|
||||
|
||||
import org.http4s._
|
||||
|
||||
object EnvMiddleware {
|
||||
|
||||
def apply[F[_]: Functor](in: HttpRoutes[F]): HttpRoutes[F] =
|
||||
NoCacheMiddleware.route(EnvMode.current.isDev)(in)
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package docspell.restserver.http4s
|
||||
|
||||
import cats.Functor
|
||||
import cats.data.Kleisli
|
||||
import cats.data.NonEmptyList
|
||||
|
||||
import docspell.common._
|
||||
|
||||
import org.http4s._
|
||||
import org.http4s.headers._
|
||||
|
||||
object NoCacheMiddleware {
|
||||
private val noCacheHeader: Header =
|
||||
`Cache-Control`(
|
||||
NonEmptyList.of(
|
||||
CacheDirective.`max-age`(Duration.zero.toScala),
|
||||
CacheDirective.`no-store`
|
||||
)
|
||||
)
|
||||
|
||||
def apply[F[_]: Functor, G[_], A](
|
||||
noCache: Boolean
|
||||
)(in: Kleisli[F, A, Response[F]]): Kleisli[F, A, Response[F]] =
|
||||
if (noCache) in.map(_.putHeaders(noCacheHeader))
|
||||
else in
|
||||
|
||||
def route[F[_]: Functor](noCache: Boolean)(in: HttpRoutes[F]): HttpRoutes[F] =
|
||||
if (noCache) in.map(_.putHeaders(noCacheHeader))
|
||||
else in
|
||||
}
|
@ -18,11 +18,12 @@ case class Flags(
|
||||
fullTextSearchEnabled: Boolean,
|
||||
maxPageSize: Int,
|
||||
maxNoteLength: Int,
|
||||
showClassificationSettings: Boolean
|
||||
showClassificationSettings: Boolean,
|
||||
uiVersion: Int
|
||||
)
|
||||
|
||||
object Flags {
|
||||
def apply(cfg: Config): Flags =
|
||||
def apply(cfg: Config, uiVersion: Int): Flags =
|
||||
Flags(
|
||||
cfg.appName,
|
||||
getBaseUrl(cfg),
|
||||
@ -32,7 +33,8 @@ object Flags {
|
||||
cfg.fullTextSearch.enabled,
|
||||
cfg.maxItemPageSize,
|
||||
cfg.maxNoteLength,
|
||||
cfg.showClassificationSettings
|
||||
cfg.showClassificationSettings,
|
||||
uiVersion
|
||||
)
|
||||
|
||||
private def getBaseUrl(cfg: Config): String =
|
||||
|
@ -14,6 +14,8 @@ import org.http4s.HttpRoutes
|
||||
import org.http4s._
|
||||
import org.http4s.dsl.Http4sDsl
|
||||
import org.http4s.headers._
|
||||
import org.http4s.util.CaseInsensitiveString
|
||||
import org.http4s.util.Writer
|
||||
import org.log4s._
|
||||
import yamusca.implicits._
|
||||
import yamusca.imports._
|
||||
@ -24,6 +26,27 @@ object TemplateRoutes {
|
||||
val `text/html` = new MediaType("text", "html")
|
||||
val `application/javascript` = new MediaType("application", "javascript")
|
||||
|
||||
val ui2Header = CaseInsensitiveString("Docspell-Ui2")
|
||||
|
||||
case class UiVersion(version: Int) extends Header.Parsed {
|
||||
val key = UiVersion
|
||||
def renderValue(writer: Writer): writer.type =
|
||||
writer.append(version)
|
||||
}
|
||||
object UiVersion extends HeaderKey.Singleton {
|
||||
type HeaderT = UiVersion
|
||||
val name = CaseInsensitiveString("Docspell-Ui")
|
||||
override def parse(s: String): ParseResult[UiVersion] =
|
||||
Either
|
||||
.catchNonFatal(s.trim.toInt)
|
||||
.leftMap(ex => ParseFailure("Invalid int header", ex.getMessage))
|
||||
.map(UiVersion.apply)
|
||||
|
||||
override def matchHeader(h: Header): Option[UiVersion] =
|
||||
if (h.name == name) parse(h.value).toOption
|
||||
else None
|
||||
}
|
||||
|
||||
trait InnerRoutes[F[_]] {
|
||||
def doc: HttpRoutes[F]
|
||||
def app: HttpRoutes[F]
|
||||
@ -53,22 +76,24 @@ object TemplateRoutes {
|
||||
} yield resp
|
||||
}
|
||||
def app =
|
||||
HttpRoutes.of[F] { case GET -> _ =>
|
||||
HttpRoutes.of[F] { case req @ GET -> _ =>
|
||||
for {
|
||||
templ <- indexTemplate
|
||||
uiv = req.headers.get(UiVersion).map(_.version).getOrElse(1)
|
||||
resp <- Ok(
|
||||
IndexData(cfg).render(templ),
|
||||
IndexData(cfg, uiv).render(templ),
|
||||
`Content-Type`(`text/html`, Charset.`UTF-8`)
|
||||
)
|
||||
} yield resp
|
||||
}
|
||||
|
||||
def serviceWorker =
|
||||
HttpRoutes.of[F] { case GET -> _ =>
|
||||
HttpRoutes.of[F] { case req @ GET -> _ =>
|
||||
for {
|
||||
templ <- swTemplate
|
||||
uiv = req.headers.get(UiVersion).map(_.version).getOrElse(1)
|
||||
resp <- Ok(
|
||||
IndexData(cfg).render(templ),
|
||||
IndexData(cfg, uiv).render(templ),
|
||||
`Content-Type`(`application/javascript`, Charset.`UTF-8`)
|
||||
)
|
||||
} yield resp
|
||||
@ -134,22 +159,28 @@ object TemplateRoutes {
|
||||
|
||||
object IndexData {
|
||||
|
||||
def apply(cfg: Config): IndexData =
|
||||
def apply(cfg: Config, uiVersion: Int): IndexData =
|
||||
IndexData(
|
||||
Flags(cfg),
|
||||
Seq(
|
||||
"/app/assets" + Webjars.fomanticslimdefault + "/semantic.min.css",
|
||||
s"/app/assets/docspell-webapp/${BuildInfo.version}/docspell.css"
|
||||
),
|
||||
Flags(cfg, uiVersion),
|
||||
chooseUi(uiVersion),
|
||||
Seq(
|
||||
"/app/assets" + Webjars.clipboardjs + "/clipboard.min.js",
|
||||
s"/app/assets/docspell-webapp/${BuildInfo.version}/docspell-app.js"
|
||||
),
|
||||
s"/app/assets/docspell-webapp/${BuildInfo.version}/favicon",
|
||||
s"/app/assets/docspell-webapp/${BuildInfo.version}/docspell.js",
|
||||
Flags(cfg).asJson.spaces2
|
||||
Flags(cfg, uiVersion).asJson.spaces2
|
||||
)
|
||||
|
||||
private def chooseUi(uiVersion: Int): Seq[String] =
|
||||
if (uiVersion == 2)
|
||||
Seq(s"/app/assets/docspell-webapp/${BuildInfo.version}/css/styles.css")
|
||||
else
|
||||
Seq(
|
||||
"/app/assets" + Webjars.fomanticslimdefault + "/semantic.min.css",
|
||||
s"/app/assets/docspell-webapp/${BuildInfo.version}/docspell.css"
|
||||
)
|
||||
|
||||
implicit def yamuscaValueConverter: ValueConverter[IndexData] =
|
||||
ValueConverter.deriveConverter[IndexData]
|
||||
}
|
||||
|
@ -4,10 +4,9 @@ import cats.data.Kleisli
|
||||
import cats.data.OptionT
|
||||
import cats.effect._
|
||||
|
||||
import org.http4s.HttpRoutes
|
||||
import org.http4s.Method
|
||||
import org.http4s.Response
|
||||
import org.http4s.StaticFile
|
||||
import docspell.common._
|
||||
|
||||
import org.http4s._
|
||||
|
||||
object WebjarRoutes {
|
||||
|
||||
@ -37,12 +36,13 @@ object WebjarRoutes {
|
||||
if (p.contains("..") || !suffixes.exists(p.endsWith(_)))
|
||||
OptionT.pure(Response.notFound[F])
|
||||
else
|
||||
StaticFile.fromResource(
|
||||
s"/META-INF/resources/webjars$p",
|
||||
blocker,
|
||||
Some(req),
|
||||
true
|
||||
)
|
||||
StaticFile
|
||||
.fromResource(
|
||||
s"/META-INF/resources/webjars$p",
|
||||
blocker,
|
||||
Some(req),
|
||||
EnvMode.current.isProd
|
||||
)
|
||||
case _ =>
|
||||
OptionT.none
|
||||
}
|
||||
|
@ -33,9 +33,8 @@
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="docspell-app">
|
||||
</div>
|
||||
<body id="docspell-app">
|
||||
<!-- everything in here gets replaced by elm, including the body tag -->
|
||||
|
||||
<script type="application/javascript">
|
||||
var storedAccount = localStorage.getItem('account');
|
||||
|
Reference in New Issue
Block a user