diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 3c0856c59..cc04693e6 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -11,10 +11,10 @@ jobs:
- uses: actions/checkout@v2.3.4
with:
fetch-depth: 0
- - name: Node 16.x
+ - name: Node 22.x
uses: actions/setup-node@v3
with:
- node-version: 16
+ node-version: 22
- name: Setup JDK
uses: actions/setup-java@v4
with:
@@ -29,8 +29,38 @@ jobs:
passphrase: ${{ secrets.PGP_PASSPHRASE }}
- name: List keys
run: gpg -K
+ - id: tag
+ run: |
+ if [[ "${GITHUB_REF#refs/tags/}" != "$GITHUB_REF" ]]; then
+ tag=${GITHUB_REF#refs/tags/}
+ else
+ tag=$(git tag --points-at "$GITHUB_SHA" | head -n1)
+ fi
+ # strip a single leading v if present
+ tag=${tag#v}
+ echo $tag
+ echo "tag=$tag" >> "$GITHUB_OUTPUT"
+ - name: Export VITE_BOX_VERSION
+ run: echo "VITE_BOX_VERSION=${{ steps.tag.outputs.tag }}" >> $GITHUB_ENV
+ - name: Install node dependencies
+ env:
+ SBT_OPTS: -XX:+UseG1GC -Xmx4G -Xss10M -XX:MaxInlineLevel=20
+ run: |
+ cd client
+ npm install
+ cd ..
+ - name: Import TS Types
+ env:
+ SBT_OPTS: -XX:+UseG1GC -Xmx4G -Xss10M -XX:MaxInlineLevel=20
+ run: |
+ sbt client/generateScalaTypes
- name: Build bundle
- run: sbt -J-Xmx4G -J-XX:MaxMetaspaceSize=1G -J-Xss10m client/fullOptJS::webpack
+ env:
+ SBT_OPTS: -XX:+UseG1GC -Xmx4G -Xss10M -XX:MaxInlineLevel=20
+ run: |
+ cd client
+ npm run build
+ cd ..
- name: Version info
run: |
sbt server/version
diff --git a/.gitignore b/.gitignore
index 9a9299794..985082ab9 100755
--- a/.gitignore
+++ b/.gitignore
@@ -74,4 +74,5 @@ node_modules
LOCAL_README.md
#let sbt handle the depenencies
-client/package-lock.json
\ No newline at end of file
+client/package-lock.json
+client/dist
\ No newline at end of file
diff --git a/build.sbt b/build.sbt
index da2999c32..775bf19a4 100755
--- a/build.sbt
+++ b/build.sbt
@@ -1,8 +1,10 @@
import com.jsuereth.sbtpgp.PgpKeys.publishSigned
+import org.scalablytyped.converter.internal.Json
+import org.scalablytyped.converter.internal.ts.PackageJson
//import xerial.sbt.Sonatype.sonatypeCentralHost
import locales.LocalesFilter
import org.scalajs.jsenv.Input.Script
-import scalajsbundler.util.JSON
+import org.scalajs.linker.interface.ModuleSplitStyle
val publishSettings = List(
Global / scalaJSStage := FullOptStage,
@@ -74,6 +76,7 @@ lazy val server: Project = project
Compile / packageBin / mainClass := Some("ch.wsl.box.rest.Boot"),
Compile / run / mainClass := Some("ch.wsl.box.rest.Boot"),
Compile / resourceDirectory := baseDirectory.value / "../resources",
+ Compile / resourceDirectory := baseDirectory.value / "../client/dist",
Test / unmanagedResourceDirectories += baseDirectory.value / "../db",
Test / unmanagedSourceDirectories += baseDirectory.value / "../db",
Test / fork := true,
@@ -81,24 +84,6 @@ lazy val server: Project = project
buildInfoKeys := Seq[BuildInfoKey](version),
buildInfoPackage := "boxInfo",
buildInfoObject := "BoxBuildInfo",
- Compile / compile := ((Compile / compile) dependsOn scalaJSPipeline).value,
- Runtime / managedClasspath += (Assets / packageBin).value,
- Assets / WebKeys.packagePrefix := "public/",
- //Comment this to avoid errors in importing project, i.e. when changing libraries
- Assets / pipelineStages := Seq(scalaJSPipeline),
- Assets / scalaJSStage := FullOptStage,
- scalaJSProjects := {
- if (sys.env.contains("DEV_SERVER") || sys.env.contains("RUNNING_TEST")) Seq() else Seq(client)
- },
-// scalaJSProjects := Seq(client),
- webpackBundlingMode := BundlingMode.Application,
- Seq("jquery","ol","bootstrap","flatpickr","quill","@fontsource/open-sans","@fortawesome/fontawesome-free","choices.js","gridstack","jspreadsheet-ce","jsuites","toolcool-range-slider","@electric-sql/pglite","@electric-sql/pglite-repl").map{ p =>
- if (!sys.env.contains("RUNNING_TEST"))
- npmAssets ++= NpmAssets.ofProject(client) { nodeModules =>
- (nodeModules / p).allPaths
- }.value
- else npmAssets := Seq()
- },
Test / testOptions ++= Seq(
Tests.Argument(TestFrameworks.ScalaTest, "-u", "target/test-reports"),
Tests.Argument(TestFrameworks.ScalaTest, "-h", "target/test-reports"),
@@ -109,7 +94,6 @@ lazy val server: Project = project
.enablePlugins(
GitVersioning,
BuildInfoPlugin,
- WebScalaJSBundlerPlugin,
SbtTwirl
)
.dependsOn(sharedJVM)
@@ -129,90 +113,26 @@ lazy val serverCacheRedis = (project in file("server-cache-redis")).settings(
lazy val client: Project = (project in file("client"))
.settings(
- name := "client",
+ name := "box-client",
scalaVersion := Settings.versions.scala213,
scalacOptions ++= Settings.scalacOptions,
resolvers += Resolver.jcenterRepo,
libraryDependencies ++= Settings.scalajsDependencies.value,
- // yes, we want to package JS dependencies
- packageJSDependencies / skip := false,
+
// use Scala.js provided launcher code to start the client app
scalaJSUseMainModuleInitializer := true,
scalaJSStage := FullOptStage,
- Compile / npmDependencies ++= Seq(
- "ol" -> "8.1.0",
- "proj4" -> "2.9.1",
- "@types/proj4" -> "2.5.3",
- "ol-ext" -> "4.0.11",
- //"@siedlerchr/types-ol-ext" -> "3.2.4",
- "jsts" -> "2.7.1",
- "@types/jsts" -> "0.17.13",
- "jquery" -> "3.4.1",
- "@types/jquery" -> "3.5.6",
- "popper.js" -> "1.16.1",
- "bootstrap" -> "4.1.3",
- "@types/bootstrap" -> "4.1.3",
- "@fortawesome/fontawesome-free" -> "5.15.4",
- "flatpickr" -> "4.6.3",
- "monaco-editor" -> "0.34.0",
- "quill" -> "1.3.7",
- "@types/quill" -> "1.3.10",
- "@fontsource/open-sans" -> "5.0.15",
- "file-saver" -> "2.0.5",
- "@types/file-saver" -> "2.0.1",
- "js-md5" -> "0.7.3",
- "@types/js-md5" -> "0.4.2",
- "striptags" -> "3.2.0",
- "toolcool-range-slider" -> "4.0.28",
- "hotkeys-js" -> "3.10.0",
- "crypto-browserify" -> "3.12.0",
- "buffer" -> "6.0.3",
- "stream-browserify" -> "3.0.0",
- "choices.js" -> "11.1.0",
- "autocompleter" -> "7.0.1",
- "xlsx-js-style" -> "1.2.0",
- "jspdf" -> "2.5.1",
- "jspdf-autotable" -> "3.5.28",
- "gridstack" -> "12.2.2",
- "jspreadsheet-ce" -> "git://github.com/jspreadsheet/ce.git#2e7389f8f6a84d260603bbac06f00bb404e1ba49", //v5.0.0
- "jsuites" -> "5.9.1",
- "@electric-sql/pglite" -> "0.2.17",
- "@electric-sql/pglite-repl" -> "0.2.17",
- "compressorjs" -> "1.2.1",
- "shapefile" -> "0.6.6",
- "@types/shapefile" -> "0.6.4",
- ),
- stIgnore += "@fontsource/open-sans",
- stIgnore += "redux",
- stIgnore += "node",
- stIgnore += "crypto-browserify",
- stIgnore += "ol-ext",
- stIgnore += "@fortawesome/fontawesome-free",
- stIgnore += "stream-browserify",
- stIgnore += "toolcool-range-slider",
- stTypescriptVersion := "4.2.4",
- stOutputPackage := "ch.wsl.typings",
- // Use library mode for fastOptJS
- //Compile / additionalNpmConfig := Map("sideEffects" -> JSON.bool(false)),
- fastOptJS / webpackBundlingMode := BundlingMode.Application,
- fastOptJS / webpackConfigFile := Some(baseDirectory.value / ".." / "dev.config.js"),
- // Use application model mode for fullOptJS
- fullOptJS / webpackBundlingMode := BundlingMode.Application,
- fullOptJS / webpackConfigFile := Some(baseDirectory.value / ".." / "prod.config.js"),
- Test / webpackConfigFile := Some(baseDirectory.value / ".." / "test.config.js"),
- Compile / npmDevDependencies ++= Seq(
- "html-webpack-plugin" -> "5.5.0",
- "webpack-merge" -> "5.8.0",
- "style-loader" -> "3.3.1",
- "css-loader" -> "6.7.1",
- "mini-css-extract-plugin" -> "2.6.1",
- "monaco-editor-webpack-plugin" -> "7.0.1",
- "file-loader" -> "6.2.0",
- ),
- webpack / version := "5.89.0",
- webpackCliVersion := "5.1.4",
- installJsdom / version := "20.0.0",
+// Compile / sourceGenerators += Def.task {
+// generateScalaTypesTask.value
+// },
+
+ Compile / unmanagedSourceDirectories += baseDirectory.value / "target" / "scala-2.13" / "src_scalablytypes" / "main",
+
+ generateScalaTypes := generateScalaTypesTask.value,
+
+ scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.ESModule).withModuleSplitStyle(ModuleSplitStyle.FewestModules)),
+
//To use jsdom headless browser uncomment the following lines
Test / requireJsDomEnv := true,
@@ -244,12 +164,13 @@ lazy val client: Project = (project in file("client"))
Tags.limit(Tags.Test,5) //browserstack limit
),
localesFilter := LocalesFilter.Selection("en", "de", "fr", "it"),
- publishTo := sonatypeCentralPublishToBundle.value
+ publishTo := sonatypeCentralPublishToBundle.value,
+ Compile / doc / sources := Seq()
+
)
.settings(publishSettings)
.enablePlugins(
ScalaJSPlugin,
- ScalablyTypedConverterGenSourcePlugin,
LocalesPlugin
)
.dependsOn(sharedJS)
@@ -348,15 +269,14 @@ lazy val box = (project in file("."))
lazy val publishAll = taskKey[Unit]("Publish all modules")
lazy val publishAllTask = {
Def.sequential(
- (client / clean),
(server / clean),
(serverCacheRedis / clean),
(serverServices / clean),
(codegen / clean),
- (client / Compile / fullOptJS / webpack),
- (client / Compile / fullOptJS),
(codegen / Compile / compile),
(sharedJVM / publishSigned),
+ (sharedJS / publishSigned),
+ (client / publishSigned),
(codegen / publishSigned),
(server / publishSigned),
(serverCacheRedis / publishSigned),
@@ -368,8 +288,6 @@ lazy val publishAllTask = {
lazy val publishAllLocal = taskKey[Unit]("Publish all modules")
lazy val publishAllLocalTask = {
Def.sequential(
- (client / Compile / fullOptJS / webpack),
- (codegen / Compile / compile),
(sharedJVM / publishLocal),
(codegen / publishLocal),
(server / publishLocal),
@@ -397,3 +315,5 @@ lazy val dropBoxTask = Def.sequential(
}
)
+lazy val generateScalaTypes = taskKey[Unit]("generateScalaTypes")
+lazy val generateScalaTypesTask = BoxScalablyTypes.generateSJSFromTS(baseDirectory)
\ No newline at end of file
diff --git a/client/index.html b/client/index.html
new file mode 100644
index 000000000..7af8856cb
--- /dev/null
+++ b/client/index.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+ Vite App
+
+
+
+
+
+
+
diff --git a/client/javascript.svg b/client/javascript.svg
new file mode 100644
index 000000000..f9abb2b72
--- /dev/null
+++ b/client/javascript.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client/libraries.css b/client/libraries.css
new file mode 100644
index 000000000..e407c2e9e
--- /dev/null
+++ b/client/libraries.css
@@ -0,0 +1,12 @@
+@import "bootstrap";
+
+@import "choices.js/src/styles/choices";
+
+@import "flatpickr/dist/flatpickr.min.css";
+@import "flatpickr/dist/themes/dark.css";
+
+@import "quill/dist/quill.snow.css";
+
+@import "ol/ol.css";
+
+@import "gridstack/dist/gridstack.min.css";
diff --git a/client/main.js b/client/main.js
new file mode 100644
index 000000000..d62873ed7
--- /dev/null
+++ b/client/main.js
@@ -0,0 +1,12 @@
+import 'scalajs:main.js'
+import './libraries.css';
+import './style.scss'
+import * as bootstrap from 'bootstrap'
+import '@fortawesome/fontawesome-free/js/all.min.js'
+import '@fontsource/open-sans';
+import '@fontsource/open-sans/400.css';
+import '@fontsource/open-sans/600.css';
+import '@fontsource/open-sans/700.css';
+import '@fontsource/open-sans/400-italic.css';
+import '@fontsource/open-sans/600-italic.css';
+import '@fontsource/open-sans/700-italic.css';
\ No newline at end of file
diff --git a/client/package.json b/client/package.json
new file mode 100644
index 000000000..c796dfd6b
--- /dev/null
+++ b/client/package.json
@@ -0,0 +1,69 @@
+{
+ "name": "box-client",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "npm run dev:app & npm run dev:worker-build & npm run dev:worker-serve",
+ "dev:app": "vite --config vite.config.app.js",
+ "dev:worker-build": "VITE_BOX_VERSION=dev vite build --config vite.config.worker.js --watch",
+ "dev:worker-serve": "http-server ./dist/ui/workers -c-1 -p 5174 --cors --base-path /ui/workers -a 0.0.0.0",
+ "build:app": "vite build --config vite.config.app.js",
+ "build:worker": "vite build --config vite.config.worker.js",
+ "build": "npm run build:worker && npm run build:app",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@electric-sql/pglite": "0.4.2",
+ "@electric-sql/pglite-repl": "0.3.2",
+ "@fontsource/open-sans": "^5.0.15",
+ "@fortawesome/fontawesome-free": "5.15.4",
+ "@tailwindcss/vite": "^4.1.18",
+ "@types/bootstrap": "4.1.3",
+ "@types/file-saver": "2.0.1",
+ "@types/jquery": "3.5.6",
+ "@types/js-md5": "0.8.0",
+ "@types/jsts": "0.17.13",
+ "@types/proj4": "2.5.3",
+ "@types/quill": "1.3.10",
+ "@types/shapefile": "0.6.4",
+ "autocompleter": "7.0.1",
+ "bootstrap": "4.1.3",
+ "buffer": "6.0.3",
+ "choices.js": "11.1.0",
+ "compressorjs": "1.2.1",
+ "crypto-browserify": "3.12.0",
+ "file-saver": "2.0.5",
+ "flatpickr": "4.6.3",
+ "gridstack": "12.2.2",
+ "hotkeys-js": "3.10.0",
+ "is-blob": "3.0.0",
+ "jquery": "3.4.1",
+ "js-md5": "0.8.3",
+ "jspdf": "4.2.1",
+ "jspdf-autotable": "5.0.7",
+ "jspreadsheet-ce": "git://github.com/jspreadsheet/ce.git#2e7389f8f6a84d260603bbac06f00bb404e1ba49",
+ "jsts": "2.7.1",
+ "jsuites": "5.9.1",
+ "monaco-editor": "0.34.0",
+ "ol": "8.1.0",
+ "ol-ext": "4.0.11",
+ "popper.js": "1.16.1",
+ "proj4": "2.9.1",
+ "quill": "1.3.7",
+ "shapefile": "0.6.6",
+ "stream-browserify": "3.0.0",
+ "string-strip-html": "13.5.3",
+ "tailwindcss": "^4.1.18",
+ "toolcool-range-slider": "4.0.28",
+ "xlsx-js-style": "1.2.0"
+ },
+ "devDependencies": {
+ "@scala-js/vite-plugin-scalajs": "^1.1.0",
+ "http-server": "^14.1.1",
+ "sass-embedded": "^1.93.2",
+ "typescript": "^4.1.6",
+ "vite": "^7.1.9",
+ "vite-plugin-static-copy": "^4.0.0"
+ }
+}
diff --git a/client/src/main/scala/ch/wsl/box/client/Main.scala b/client/src/main/scala/ch/wsl/box/client/Main.scala
index 75225b622..21826a1f6 100644
--- a/client/src/main/scala/ch/wsl/box/client/Main.scala
+++ b/client/src/main/scala/ch/wsl/box/client/Main.scala
@@ -1,6 +1,7 @@
package ch.wsl.box.client
import ch.wsl.box.client.db.DB
+import ch.wsl.box.client.services.impl.DaoLocalDbImpl
import ch.wsl.box.client.services.{BrowserConsole, ClientConf, Labels, Notification, REST, UI}
import ch.wsl.box.client.styles.{AutocompleteStyles, ChoicesStyles, OpenLayersStyles}
import ch.wsl.box.client.utils._
@@ -9,6 +10,7 @@ import io.udash.wrappers.jquery._
import org.scalajs.dom
import org.scalajs.dom.{Element, WebSocket, document, window}
import scribe.{Level, Logger, Logging}
+import wvlet.airframe.Design
import scala.concurrent.Future
import scala.scalajs.js
@@ -21,7 +23,13 @@ object Main extends Logging {
def main(args: Array[String]): Unit = {
val moduleName = window.asInstanceOf[js.Dynamic].boxUiModule.asInstanceOf[String]
- Context.init(Module.byName(moduleName))
+ val design = Module.byName(moduleName)
+ boot(design)
+ }
+
+ def boot(module:Design) = {
+
+ Context.init(module)
println(
s"""
@@ -48,34 +56,37 @@ object Main extends Logging {
})
//window.onerror = ErrorHandler.onError
-
}
def setupUI(): Future[Unit] = {
+ val (_version,_appVersion) = {
+ val local = dom.window.asInstanceOf[js.Dynamic].boxVersion.asInstanceOf[js.UndefOr[String]]
+ local.toOption match {
+ case Some(v) => (Future.successful(v),Future.successful(v))
+ case None => (services.rest.version(),services.rest.appVersion())
+ }
+ }
for {
_ <- services.clientSession.refreshSession()
- appVersion <- services.rest.appVersion()
- version <- services.rest.version()
+ appVersion <- _appVersion
+ version <- _version
_ <- services.rest.conf().map{ conf =>
ClientConf.load(conf,version,appVersion) //needs to be loaded before labels, fix #91
}
uiConf <- services.rest.ui()
labels <- services.rest.labels(services.clientSession.lang())
- _ <- if(ClientConf.localDb) DB.init() else Future.successful()
+ _ <- if(services.data.name == DaoLocalDbImpl.name) DB.init(version) else Future.successful()
} yield {
Logger.root.clearHandlers().clearModifiers().withHandler(minimumLevel = Some(ClientConf.loggerLevel)).replace()
println(s"Setting logger level to ${ClientConf.loggerLevel}")
//loads datetime picker
- ch.wsl.typings.bootstrap.bootstrapRequire
//ch.wsl.typings.toolcoolRangeSlider.toolcoolRangeSliderRequire
-
-
Labels.load(labels)
UI.load(uiConf)
@@ -85,6 +96,7 @@ object Main extends Logging {
val CssSettings = scalacss.devOrProdDefaults
import CssSettings._
+
val mainStyle = document.createElement("style")
mainStyle.innerText = ClientConf.style.render(cssStringRenderer,cssEnv)
@@ -130,10 +142,9 @@ object Main extends Logging {
document.body.appendChild(autocompleteStyle)
document.body.appendChild(stepperStyle)
- val app = document.createElement("div")
+ val app = document.getElementById("app")
BrowserConsole.log(app)
- document.body.appendChild(app)
BrowserConsole.log(document.body)
applicationInstance.run(app)
diff --git a/client/src/main/scala/ch/wsl/box/client/Module.scala b/client/src/main/scala/ch/wsl/box/client/Module.scala
index 18969de8a..40dfb58e4 100644
--- a/client/src/main/scala/ch/wsl/box/client/Module.scala
+++ b/client/src/main/scala/ch/wsl/box/client/Module.scala
@@ -1,6 +1,8 @@
package ch.wsl.box.client
import ch.wsl.box.client.services.{ClientSession, DataAccessObject, HttpClient, Navigator, Notification, NotificationChannel, NotificationWebSocket, REST}
import ch.wsl.box.client.services.impl.{DaoLocalDbImpl, DaoPassthroughImpl, HttpClientImpl, RestImpl}
+import ch.wsl.box.client.styles.{BoxStyle, BoxStyleFactory, GlobalStyleFactory}
+import ch.wsl.box.client.views.components.{BoxMainLayout, MainLayout}
import ch.wsl.box.model.shared.AvailableUIModule
import wvlet.airframe._
@@ -18,6 +20,8 @@ object Module {
.bind[ClientSession].toEagerSingleton
.bind[Navigator].toEagerSingleton
.bind[NotificationChannel].to[NotificationWebSocket]
+ .bind[BoxStyleFactory].to[GlobalStyleFactory]
+ .bind[MainLayout].to[BoxMainLayout]
val prod = newDesign
.bind[HttpClient].to[HttpClientImpl]
@@ -26,5 +30,6 @@ object Module {
.bind[ClientSession].toEagerSingleton
.bind[Navigator].toEagerSingleton
.bind[NotificationChannel].to[NotificationWebSocket]
-
+ .bind[BoxStyleFactory].to[GlobalStyleFactory]
+ .bind[MainLayout].to[BoxMainLayout]
}
diff --git a/client/src/main/scala/ch/wsl/box/client/db/DB.scala b/client/src/main/scala/ch/wsl/box/client/db/DB.scala
index c3603dafb..9559ad0d8 100644
--- a/client/src/main/scala/ch/wsl/box/client/db/DB.scala
+++ b/client/src/main/scala/ch/wsl/box/client/db/DB.scala
@@ -1,8 +1,9 @@
package ch.wsl.box.client.db
-import ch.wsl.box.client.services.BrowserConsole
+import ch.wsl.box.client.routes.Routes
+import ch.wsl.box.client.services.{BrowserConsole, ClientConf}
+import ch.wsl.box.client.vendors.PGliteWorker
import ch.wsl.typings.electricSqlPglite.mod.PGlite
-import ch.wsl.typings.electricSqlPglite.workerMod.PGliteWorker
import org.scalajs.dom
import org.scalajs.dom.{URL, Worker, WorkerOptions, WorkerType}
@@ -12,18 +13,19 @@ import scala.scalajs.js
object DB {
- private val worker_options = new WorkerOptions {}
- worker_options.`type` = WorkerType.module
- //val worker_url = new URL("./postgres.worker.js",js.`import`.meta.asInstanceOf[String]).asInstanceOf[String]
- private val worker = new Worker("./postgres.worker.js",worker_options)
+ var connection:PGliteWorker = null
+ var localRecord: LocalRecordDAO = null
- val connection = new PGliteWorker(worker)
+ def init(version:String)(implicit ex:ExecutionContext) = {
- val localRecord = new LocalRecordDAO(connection)
+ val worker_options = new WorkerOptions {}
+ worker_options.`type` = WorkerType.module
- def init()(implicit ex:ExecutionContext) = {
+ val worker = new Worker(s"${Routes.baseUri}ui/workers/postgres.worker.${version}.js?version=$version&appId=${ClientConf.applicationId}",worker_options)
+ connection = new PGliteWorker(worker)
+ localRecord = new LocalRecordDAO(connection)
for {
result <- localRecord.init()
diff --git a/client/src/main/scala/ch/wsl/box/client/db/LocalRecord.scala b/client/src/main/scala/ch/wsl/box/client/db/LocalRecord.scala
index e04e65207..c0dae9d79 100644
--- a/client/src/main/scala/ch/wsl/box/client/db/LocalRecord.scala
+++ b/client/src/main/scala/ch/wsl/box/client/db/LocalRecord.scala
@@ -1,8 +1,8 @@
package ch.wsl.box.client.db
import ch.wsl.box.client.services.BrowserConsole
+import ch.wsl.box.client.vendors.PGliteWorker
import ch.wsl.box.model.shared.{EntityKind, JSONID}
-import ch.wsl.typings.electricSqlPglite.workerMod.PGliteWorker
import io.circe.Json
import scribe.Logging
diff --git a/client/src/main/scala/ch/wsl/box/client/geo/MapGeolocation.scala b/client/src/main/scala/ch/wsl/box/client/geo/MapGeolocation.scala
index 41eb29ede..5d2216432 100644
--- a/client/src/main/scala/ch/wsl/box/client/geo/MapGeolocation.scala
+++ b/client/src/main/scala/ch/wsl/box/client/geo/MapGeolocation.scala
@@ -3,22 +3,18 @@ package ch.wsl.box.client.geo
import ch.wsl.typings.ol
import ch.wsl.typings.ol.geomMod.Point
import ch.wsl.typings.ol.{controlControlMod, controlMod, geolocationMod, geomGeometryMod, geomMod, imageMod, layerBaseVectorMod, layerMod, mod, renderFeatureMod, sourceMod, sourceVectorMod, styleMod}
-import ch.wsl.typings.std.PositionOptions
-import org.scalajs.dom.{Event, HTMLInputElement}
+import org.scalajs.dom.{Event, HTMLInputElement, PositionOptions}
import scalatags.JsDom.all.{`class`, `type`, div, input, onchange, style}
import scala.scalajs.js
-
import scalatags.JsDom.all._
import io.udash._
class MapGeolocation(map:mod.Map) {
- val positionOptions = PositionOptions().setEnableHighAccuracy(true)
val geolocation = new mod.Geolocation(
geolocationMod.Options()
.setProjection(map.getView().getProjection())
- .setTrackingOptions(positionOptions.asInstanceOf[org.scalajs.dom.PositionOptions])
)
val accuracyFeature = new mod.Feature[geomMod.Geometry]()
diff --git a/client/src/main/scala/ch/wsl/box/client/routes/Routes.scala b/client/src/main/scala/ch/wsl/box/client/routes/Routes.scala
index 68f1764ec..80bc49973 100755
--- a/client/src/main/scala/ch/wsl/box/client/routes/Routes.scala
+++ b/client/src/main/scala/ch/wsl/box/client/routes/Routes.scala
@@ -38,17 +38,7 @@ object Routes extends Logging {
def fullUrl = dom.document.asInstanceOf[js.Dynamic].baseURI.asInstanceOf[String]
def originUrl = dom.window.location.origin
- def baseUri = {
- val bu = fullUrl
- if(bu.contains("//")) {
- bu.split("/").drop(3).toList match {
- case Nil => "/"
- case x => x.mkString("/","/","/")
- }
- } else {
- bu
- }
- }
+ def baseUri = ClientConf.frontendUrl
def removeBase(s:String):String = {
val result = s.stripPrefix(baseUri.stripSuffix("/"))
@@ -62,7 +52,7 @@ object Routes extends Logging {
}
def wsV1(topic:String):String = {
- fullUrl.replace("http","ws") + "api/v1/notifications/"+topic
+ originUrl.replace("http","ws") + "/api/v1/notifications/"+topic
}
def apply(kind:String, entityName:String,public:Boolean) = new Routes{
diff --git a/client/src/main/scala/ch/wsl/box/client/services/BoxFileReader.scala b/client/src/main/scala/ch/wsl/box/client/services/BoxFileReader.scala
index 824d853c3..322b9c346 100644
--- a/client/src/main/scala/ch/wsl/box/client/services/BoxFileReader.scala
+++ b/client/src/main/scala/ch/wsl/box/client/services/BoxFileReader.scala
@@ -28,7 +28,7 @@ object BoxFileReader {
}
- def readAsDataURL(blob:Blob)(implicit ex:ExecutionContext):Future[js.Any] = _read(_.readAsDataURL(blob))
+ def readAsDataURL(blob:Blob)(implicit ex:ExecutionContext):Future[String] = _read(_.readAsDataURL(blob)).map(_.toString)
def readAsArrayBuffer(blob:Blob)(implicit ex:ExecutionContext):Future[js.Any] = _read(_.readAsArrayBuffer(blob))
def readAsText(blob:Blob)(implicit ex:ExecutionContext):Future[String] = _read(_.readAsText(blob)).map(_.toString)
diff --git a/client/src/main/scala/ch/wsl/box/client/services/ClientConf.scala b/client/src/main/scala/ch/wsl/box/client/services/ClientConf.scala
index b1f18f094..d511d6f31 100644
--- a/client/src/main/scala/ch/wsl/box/client/services/ClientConf.scala
+++ b/client/src/main/scala/ch/wsl/box/client/services/ClientConf.scala
@@ -1,17 +1,21 @@
package ch.wsl.box.client.services
+import ch.wsl.box.client.Context.services
+
import java.sql.Timestamp
import java.time.temporal.ChronoUnit
import ch.wsl.box.client.styles.constants.StyleConstants
import ch.wsl.box.client.styles.constants.StyleConstants.{ChildProperties, Colors}
-import ch.wsl.box.client.styles.{GlobalStyleFactory, StyleConf}
+import ch.wsl.box.client.styles.{BoxStyle, GlobalStyleFactory, StyleConf}
import ch.wsl.box.model.shared.JSONFieldTypes
import ch.wsl.box.model.shared.oidc.OIDCFrontendConf
import io.circe._
import io.circe.parser._
import io.circe.generic.auto._
+import org.scalajs.dom
import scribe.Level
+import scala.scalajs.js
import scala.util.Try
/**
@@ -25,11 +29,13 @@ object ClientConf {
private var conf:Map[String,String] = Map()
private var _version:String = ""
private var _appVersion:String = ""
+ private var _style:BoxStyle = null
def load(table:Map[String,String],version:String,appVersion:String) = {
conf = table
_version = version
_appVersion = appVersion
+ _style = services.style.build(scalacss.devOrProdDefaults)
}
def loggerLevel = conf.get("client.logger.level") match {
@@ -40,11 +46,11 @@ object ClientConf {
case _ => Level.Warn
}
- def localDb:Boolean = Try(conf("local.db").toBoolean).getOrElse(true)
-
def version: String = _version
def appVersion: String = _appVersion
+ def applicationId:String = conf.get("application_id").getOrElse("box-app")
+
def pageLength: Int = Try(conf("page_length").toInt).getOrElse(30)
// def lookupMaxRows = Try(conf("fk_rows").toInt).getOrElse(30)
@@ -59,7 +65,7 @@ object ClientConf {
def labelAlign: String = Try(conf("label.align")).getOrElse("left")
def menuSeparator: String = Try(conf("menu.separator")).getOrElse(" ")
- def frontendUrl: String = Try(conf("frontendUrl")).getOrElse("http://localhost:8080")
+ def frontendUrl: String = dom.window.asInstanceOf[js.Dynamic].boxFrontendUrl.asInstanceOf[String]
def colorMain: String = Try(conf("color.main")).getOrElse("#006268")
def colorMainText: String = Try(conf("color.main.text")).getOrElse("#ffffff")
@@ -91,7 +97,8 @@ object ClientConf {
inputWidth
)
- lazy val style = GlobalStyleFactory.GlobalStyles(styleConf)
+ //lazy val style = new GlobalStyleFactory.GlobalStyles(styleConf)
+ def style = _style
def filterPrecisionDatetime: String = Try(conf("filter.precision.datetime").toUpperCase).toOption match {
case Some("DATE") => JSONFieldTypes.DATE
diff --git a/client/src/main/scala/ch/wsl/box/client/services/ClientSession.scala b/client/src/main/scala/ch/wsl/box/client/services/ClientSession.scala
index e7cdefef1..20d13ebac 100644
--- a/client/src/main/scala/ch/wsl/box/client/services/ClientSession.scala
+++ b/client/src/main/scala/ch/wsl/box/client/services/ClientSession.scala
@@ -255,6 +255,8 @@ class ClientSession(rest:REST,httpClient: HttpClient) extends Logging {
get[Seq[TableChildElement]](TABLECHILD_OPEN).toSeq.flatten.filterNot(_ == tc)
)
+
+
def lang():String = {
Routes.urlParams.get(LANG).foreach(l =>
@@ -271,9 +273,12 @@ class ClientSession(rest:REST,httpClient: HttpClient) extends Logging {
}
}
+ val langProperty = Property(lang())
+
def setLang(lang:String) = rest.labels(lang).map{ labels =>
Labels.load(labels)
dom.window.sessionStorage.setItem(LANG,lang)
+ langProperty.set(lang)
Context.applicationInstance.reload()
}
diff --git a/client/src/main/scala/ch/wsl/box/client/services/DataAccessObject.scala b/client/src/main/scala/ch/wsl/box/client/services/DataAccessObject.scala
index 512136552..8fdba8fc9 100644
--- a/client/src/main/scala/ch/wsl/box/client/services/DataAccessObject.scala
+++ b/client/src/main/scala/ch/wsl/box/client/services/DataAccessObject.scala
@@ -9,6 +9,9 @@ import scala.concurrent.{ExecutionContext, Future}
case class Record(data:Json,local_version:Boolean)
trait DataAccessObject {
+
+ def name:String
+
def get(kind:String, lang:String, entity:String, id:JSONID,public:Boolean = false)(implicit ec:ExecutionContext):Future[Record]
def insert(kind:String, lang:String, entity:String, data:Json, public:Boolean)(implicit ec:ExecutionContext): Future[Json]
def update(kind:String, lang:String, entity:String, id:JSONID, data:Json,public:Boolean = false)(implicit ec:ExecutionContext):Future[Json]
diff --git a/client/src/main/scala/ch/wsl/box/client/services/HttpClient.scala b/client/src/main/scala/ch/wsl/box/client/services/HttpClient.scala
index 0a131f19b..b5d31cd3c 100755
--- a/client/src/main/scala/ch/wsl/box/client/services/HttpClient.scala
+++ b/client/src/main/scala/ch/wsl/box/client/services/HttpClient.scala
@@ -4,7 +4,7 @@ import java.io.ByteArrayInputStream
import ch.wsl.box.client.{Context, IndexState}
import ch.wsl.box.model.shared.errors.{ExceptionReport, GenericExceptionReport, JsonDecoderExceptionReport, SQLExceptionReport}
import org.scalajs.dom
-import org.scalajs.dom.{File, FormData, XMLHttpRequest}
+import org.scalajs.dom.{Blob, File, FormData, XMLHttpRequest}
import scribe.Logging
import scala.concurrent.{ExecutionContext, Future, Promise}
@@ -28,6 +28,7 @@ trait HttpClient{
def sendFile[T](url: String, file: File)(implicit decoder: io.circe.Decoder[T], ex:ExecutionContext):Future[T]
def sendRaw[T](url: String, data: js.Any)(implicit decoder: io.circe.Decoder[T], ex:ExecutionContext):Future[T]
def setHandleAuthFailure(f:() => Unit)
+ def getBlob(url:String)(implicit ex:ExecutionContext):Future[Blob]
}
diff --git a/client/src/main/scala/ch/wsl/box/client/services/Labels.scala b/client/src/main/scala/ch/wsl/box/client/services/Labels.scala
index b96549d58..15ddb4dd8 100644
--- a/client/src/main/scala/ch/wsl/box/client/services/Labels.scala
+++ b/client/src/main/scala/ch/wsl/box/client/services/Labels.scala
@@ -144,6 +144,7 @@ object Labels {
def confirmRevert = get(SharedLabels.entity.confirmRevert)
def csv = get(SharedLabels.entity.csv)
def xls = get(SharedLabels.entity.xls)
+ def pdf = get(SharedLabels.entity.pdf)
def importxls = get(SharedLabels.entity.importxls)
def shp = get(SharedLabels.entity.shp)
def geoPackage = get(SharedLabels.entity.geopackage)
@@ -164,6 +165,7 @@ object Labels {
def search = get(SharedLabels.popup.search)
def close = get(SharedLabels.popup.close)
def remove = get(SharedLabels.popup.remove)
+ def back = get(SharedLabels.popup.back)
}
object home {
diff --git a/client/src/main/scala/ch/wsl/box/client/services/Notification.scala b/client/src/main/scala/ch/wsl/box/client/services/Notification.scala
index b78705da1..84bc5372b 100755
--- a/client/src/main/scala/ch/wsl/box/client/services/Notification.scala
+++ b/client/src/main/scala/ch/wsl/box/client/services/Notification.scala
@@ -6,6 +6,7 @@ import io.circe.parser._
import io.circe.generic.auto._
import io.udash._
import org.scalajs.dom.WebSocket
+import scribe.Logging
import scala.concurrent.duration._
import scala.scalajs.js.timers.setTimeout
@@ -35,7 +36,7 @@ class NoNotification() extends NotificationChannel {
override def close(): Unit = {}
}
-class NotificationWebSocket() extends NotificationChannel {
+class NotificationWebSocket() extends NotificationChannel with Logging {
private var socket:WebSocket = null
@@ -45,7 +46,9 @@ class NotificationWebSocket() extends NotificationChannel {
socket.close()
}
- socket = new WebSocket(Routes.wsV1("box-client"))
+ val url = Routes.wsV1("box-client")
+ logger.info(s"Opening websocket on $url")
+ socket = new WebSocket(url)
socket.onmessage = (msg => {
diff --git a/client/src/main/scala/ch/wsl/box/client/services/PDF.scala b/client/src/main/scala/ch/wsl/box/client/services/PDF.scala
new file mode 100644
index 000000000..407a73720
--- /dev/null
+++ b/client/src/main/scala/ch/wsl/box/client/services/PDF.scala
@@ -0,0 +1,97 @@
+package ch.wsl.box.client.services
+
+import ch.wsl.box.client.Context.services
+import ch.wsl.box.client.routes.Routes
+import ch.wsl.box.model.shared.{ExportMode, JSONQuery}
+import io.circe.syntax.EncoderOps
+import io.circe.generic.auto._
+
+import scala.scalajs.js.URIUtils
+import JSONQuery._
+import ch.wsl.box.client.utils.ImageUtils
+import ch.wsl.typings.jspdfAutotable.anon.PartialStyles
+import ch.wsl.typings.jspdfAutotable.mod.{HorizontalPageBreakBehaviourType, RowInput, UserOptions}
+import kantan.csv._
+import kantan.csv.ops._
+
+import scala.concurrent.ExecutionContext
+import scala.scalajs.js.JSConverters._
+import scala.scalajs.js
+import scalatags.JsDom.all._
+import ch.wsl.typings.jspdf.mod.jsPDF
+import com.avsystem.commons.Future
+
+object PDF {
+
+ import ch.wsl.box.client.Context._
+
+
+ def renderTable(title:String,header:Seq[String],body:Seq[Seq[String]])(implicit ec:ExecutionContext) = {
+
+ val logo = UI.logo match {
+ case Some(l) => for{
+ img <- services.httpClient.getBlob(ClientConf.frontendUrl + l)
+ resized <- ImageUtils.setHeight(img,60)
+ base64 <- BoxFileReader.readAsDataURL(resized)
+ dim <- ImageUtils.dimensions(base64)
+ } yield {
+ println(dim)
+ Some((base64,dim))
+ }
+ case None => Future.successful(None)
+ }
+
+ logo.foreach { logo =>
+
+ val doc = new jsPDF(ch.wsl.typings.jspdf.jspdfStrings.landscape,ch.wsl.typings.jspdf.jspdfStrings.mm)
+
+ val data = body.map(_.toJSArray).toJSArray.asInstanceOf[js.Array[RowInput]]
+
+ ch.wsl.typings.jspdfAutotable.mod.default(doc, UserOptions()
+ .setHead(js.Array(header.toJSArray).asInstanceOf[js.Array[RowInput]])
+ .setBody(data)
+ .setWillDrawPage(data => {
+ doc.setFontSize(12)
+ doc.setTextColor(40)
+ logo.foreach { case (i,d) =>
+ println(d)
+ doc.addImage(i, "JPEG", data.settings.margin.left, 6, d.scaled_w(10),10)
+ }
+ doc.text(s"${UI.title.getOrElse("Box Framework")} - $title", data.settings.margin.left + 10 + logo.map(_._2.scaled_w(10)).getOrElse(0), 12)
+ ()
+ })
+ .setMargin(js.Array[Double](20, 10, 10, 10))
+ .setStyles(PartialStyles().setCellPadding(0.5).setFontSize(9))
+ .setHeadStyles(PartialStyles().setFillColor(ClientConf.colorMain).setTextColor(ClientConf.colorMainText))
+ .setHorizontalPageBreak(true)
+ .setHorizontalPageBreakRepeat(0)
+ .setHorizontalPageBreakBehaviour(HorizontalPageBreakBehaviourType.immediately)
+ )
+
+ val pages: Int = doc.internal.asInstanceOf[js.Dynamic].getNumberOfPages().asInstanceOf[Int]
+ for (i <- 1 to pages) {
+ doc.setFontSize(8)
+ val h = doc.internal.pageSize.getHeight()
+ val w = doc.internal.pageSize.getWidth()
+ doc.setPage(i)
+ doc.text(s"$i/$pages", w - 30, h - 10)
+ }
+
+ doc.save(s"$title.pdf")
+ }
+ }
+
+ def table(kind:String,modelName:String,fields:Seq[String],query:JSONQuery)(implicit ec:ExecutionContext) = {
+ val csv = Routes.apiV1(
+ s"/$kind/${services.clientSession.lang()}/$modelName/csv?fk=${ExportMode.RESOLVE_FK}&fields=${fields.mkString(",")}&q=${URIUtils.encodeURI(query.asJson.noSpaces)}".replaceAll("\n","")
+ )
+ services.httpClient.get[String](csv).map{ result =>
+ result.asUnsafeCsvReader[Seq[String]](rfc).toList match {
+ case header :: body => renderTable(modelName,header,body)
+ }
+
+
+ }
+
+ }
+}
diff --git a/client/src/main/scala/ch/wsl/box/client/services/ServiceModule.scala b/client/src/main/scala/ch/wsl/box/client/services/ServiceModule.scala
index 1935ca981..78d1ccf76 100644
--- a/client/src/main/scala/ch/wsl/box/client/services/ServiceModule.scala
+++ b/client/src/main/scala/ch/wsl/box/client/services/ServiceModule.scala
@@ -1,5 +1,7 @@
package ch.wsl.box.client.services
+import ch.wsl.box.client.styles.BoxStyleFactory
+import ch.wsl.box.client.views.components.MainLayout
import wvlet.airframe._
trait ServiceModule {
@@ -9,4 +11,6 @@ trait ServiceModule {
val clientSession = bind[ClientSession]
val navigator = bind[Navigator]
val notification = bind[NotificationChannel]
+ val style = bind[BoxStyleFactory]
+ val layout = bind[MainLayout]
}
diff --git a/client/src/main/scala/ch/wsl/box/client/services/impl/DaoLocalDbImpl.scala b/client/src/main/scala/ch/wsl/box/client/services/impl/DaoLocalDbImpl.scala
index 73e5d6125..a11388b88 100644
--- a/client/src/main/scala/ch/wsl/box/client/services/impl/DaoLocalDbImpl.scala
+++ b/client/src/main/scala/ch/wsl/box/client/services/impl/DaoLocalDbImpl.scala
@@ -9,8 +9,14 @@ import io.circe.Json
import scala.concurrent.{ExecutionContext, Future}
+object DaoLocalDbImpl {
+ val name = "postgres-local"
+}
+
class DaoLocalDbImpl(rest:REST, clientSession: ClientSession) extends DataAccessObject {
+ override def name: String = DaoLocalDbImpl.name
+
override def get(kind: String, lang: String, entity: String, id: JSONID, public: Boolean)(implicit ec: ExecutionContext): Future[Record] = {
DB.localRecord.get(LocalRecordKey(id.asString,kind,entity)).flatMap {
case Some(value) => Future.successful(Record(value.data,true))
diff --git a/client/src/main/scala/ch/wsl/box/client/services/impl/DaoPassthroughImpl.scala b/client/src/main/scala/ch/wsl/box/client/services/impl/DaoPassthroughImpl.scala
index 99c96db50..5db0bd230 100644
--- a/client/src/main/scala/ch/wsl/box/client/services/impl/DaoPassthroughImpl.scala
+++ b/client/src/main/scala/ch/wsl/box/client/services/impl/DaoPassthroughImpl.scala
@@ -10,8 +10,14 @@ import io.circe.Json
import scala.concurrent.{ExecutionContext, Future}
+object DaoPassthroughImpl {
+ val name = "online"
+}
+
class DaoPassthroughImpl(rest:REST, clientSession: ClientSession) extends DataAccessObject {
+ override def name: String = DaoPassthroughImpl.name
+
override def get(kind: String, lang: String, entity: String, id: JSONID, public: Boolean)(implicit ec: ExecutionContext): Future[Record] = {
rest.get(kind, lang, entity, id, public).map(x => Record(x,false))
}
diff --git a/client/src/main/scala/ch/wsl/box/client/services/impl/HttpClientImpl.scala b/client/src/main/scala/ch/wsl/box/client/services/impl/HttpClientImpl.scala
index e6f576057..0b1941ee8 100644
--- a/client/src/main/scala/ch/wsl/box/client/services/impl/HttpClientImpl.scala
+++ b/client/src/main/scala/ch/wsl/box/client/services/impl/HttpClientImpl.scala
@@ -3,9 +3,9 @@ package ch.wsl.box.client.services.impl
import ch.wsl.box.client.services.HttpClient.Response
import ch.wsl.box.client.services.{BrowserConsole, HttpClient, Labels, Notification, RunNowExecutionContext}
import ch.wsl.box.model.shared.errors.{ExceptionReport, GenericExceptionReport, JsonDecoderExceptionReport, SQLExceptionReport}
-import io.circe.Decoder
+import io.circe.{Decoder, Json}
import org.scalajs.dom
-import org.scalajs.dom.{File, FormData, XMLHttpRequest}
+import org.scalajs.dom.{Blob, File, FileReader, FormData, HttpMethod, RequestInit, XMLHttpRequest}
import scribe.Logging
import scala.concurrent.{ExecutionContext, ExecutionContextExecutor, Future, Promise}
@@ -195,4 +195,14 @@ class HttpClientImpl extends HttpClient with Logging {
override def setHandleAuthFailure(f: () => Unit): Unit = {
handleAuthFailure = f
}
+
+ override def getBlob(url: String)(implicit ex:ExecutionContext): Future[Blob] = {
+ for{
+ req <- dom.fetch(url,new RequestInit {
+ method = HttpMethod.GET
+ }).toFuture
+ blob <- req.blob().toFuture
+ } yield blob
+
+ }
}
\ No newline at end of file
diff --git a/client/src/main/scala/ch/wsl/box/client/styles/BoxStyle.scala b/client/src/main/scala/ch/wsl/box/client/styles/BoxStyle.scala
new file mode 100644
index 000000000..0dd330b30
--- /dev/null
+++ b/client/src/main/scala/ch/wsl/box/client/styles/BoxStyle.scala
@@ -0,0 +1,152 @@
+package ch.wsl.box.client.styles
+
+import scalacss.internal.{Env, Renderer}
+import scalacss.internal.mutable.{Register, Settings}
+import scalacss.{StyleA, StyleSheet}
+
+trait BoxStyleFactory {
+ def build(settings: Settings):BoxStyle
+}
+
+trait BoxStyle {
+
+ val inputHighlight: StyleA
+ val inputInvalid: StyleA
+ val spaceBetween: StyleA
+ val flexContainer: StyleA
+ val sidebarRightContent: StyleA
+ val sidebar: StyleA
+ val spaceAfter: StyleA
+ val navigationBlock: StyleA
+ val textNoWrap: StyleA
+ val dataChanged: StyleA
+ val formTitle: StyleA
+ val formTitleLight: StyleA
+ val chipLink: StyleA
+ val chip: StyleA
+ val checkboxWidget: StyleA
+ val jsonMetadataRendered: StyleA
+ val preformatted: StyleA
+ val formHeader: StyleA
+ val dateTimePicker: StyleA
+ val dateTimePickerFullWidth: StyleA
+ val tableHeaderFixed: StyleA
+ val smallCells: StyleA
+ val tableCellActions: StyleA
+ val rowStyle: StyleA
+ val tableHeader: StyleA
+ val numberCells: StyleA
+ val textCells: StyleA
+ val lookupCells: StyleA
+ val dateCells: StyleA
+ val noPadding: StyleA
+ val smallBottomMargin: StyleA
+ val mediumBottomMargin: StyleA
+ val subBlock: StyleA
+ val block: StyleA
+ val innerBlock: StyleA
+ val withBorder: StyleA
+ val removeFlexChild: StyleA
+ val tableContainer: StyleA
+ val table: StyleA
+ val field: StyleA
+ val fieldHighlight: StyleA
+ val removeFieldMargin: StyleA
+ val removeFieldAndBlockMargin: StyleA
+ val distributionContrainer: StyleA
+ val distributionChild: StyleA
+ val boxedLink: StyleA
+ val notificationArea: StyleA
+ val notification: StyleA
+ val headerLogo: StyleA
+ val headerTitle: StyleA
+ val linkHeaderFooter: StyleA
+ val fullHeightMax: StyleA
+ val tableWrapper: StyleA
+ val fullHeight: StyleA
+ val loading: StyleA
+ val noMargin: StyleA
+ val subform: StyleA
+ val childTable: StyleA
+ val childTableTr: StyleA
+ val childTableTd: StyleA
+ val childTableHeader: StyleA
+ val childTableAction: StyleA
+ val childFormTableTr: StyleA
+ val childFormTableTd: StyleA
+ val boxIconButton: StyleA
+ val boxIconButtonDanger: StyleA
+ val boxButton: StyleA
+ val boxNavigationLabel: StyleA
+ val spacedList: StyleA
+ val boxButtonImportant: StyleA
+ val boxButtonDanger: StyleA
+ val popupButton: StyleA
+ val popupEntiresList: StyleA
+ val popupEntiresItem: StyleA
+ val fullWidth: StyleA
+ val maxFullWidth: StyleA
+ val filterTableSelect: StyleA
+ val imageThumb: StyleA
+ val noBullet: StyleA
+ val noMobile:StyleA
+ val mobileBoxAction:StyleA
+ val mobileMenu:StyleA
+ val mobileOnly:StyleA
+ val showFooterActionOnMobile:StyleA
+ val adminCreateForm:StyleA
+ val adminFormEditAction:StyleA
+ val mapPopup:StyleA
+ val mapSearch:StyleA
+ val mapFullscreen:StyleA
+ val mapLayerSelect:StyleA
+ val mapLayerSelectFullscreen:StyleA
+ val mapInfo:StyleA
+ val mapInfoChild:StyleA
+ val mapGeomAction:StyleA
+ val mapButton:StyleA
+ val mapTable:StyleA
+ val controlButtons:StyleA
+ val controlInputs:StyleA
+ val controlButtonsBottom:StyleA
+ val xyButtonOnTable:StyleA
+ val simpleCheckbox:StyleA
+ val dropFileZone:StyleA
+ val dropFileZoneDropping:StyleA
+ val editableTableEditButton:StyleA
+ val simpleInputBottomBorder:StyleA
+ val simpleInput:StyleA
+ val editor:StyleA
+ val tristateCheckBox:StyleA
+ val tristatePositive:StyleA
+ val tristateNegative:StyleA
+ val label50:StyleA
+ val inputRightLabel:StyleA
+ val notNullable:StyleA
+ val thOver:StyleA
+ val childDuplicateButton:StyleA
+ val childAddButtonBoxed:StyleA
+ val childAddButton:StyleA
+ val childRemoveButton:StyleA
+ val childMoveButton:StyleA
+ val twoListRight:StyleA
+ val twoListLeft:StyleA
+ val twoListContainer:StyleA
+ val twoListButton:StyleA
+ val twoListElement:StyleA
+ val editableTableMulti:StyleA
+ val queryBuilderContainer:StyleA
+ val adminConditionBlock:StyleA
+ val centredContent:StyleA
+ val smallLabelRequired:StyleA
+ val labelRequired:StyleA
+ val labelNonRequred:StyleA
+ val margin0Auto:StyleA
+ val mobileFooter:StyleA
+ val hrThin:StyleA
+ val mobileBoxActionPanel:StyleA
+ val sidebarButton:StyleA
+ val showHide:StyleA
+
+ def render[Out](implicit r: Renderer[Out], env: Env):Out
+}
diff --git a/client/src/main/scala/ch/wsl/box/client/styles/GlobalStyles.scala b/client/src/main/scala/ch/wsl/box/client/styles/GlobalStyles.scala
index 789e4c89e..4cfcf29e1 100755
--- a/client/src/main/scala/ch/wsl/box/client/styles/GlobalStyles.scala
+++ b/client/src/main/scala/ch/wsl/box/client/styles/GlobalStyles.scala
@@ -2,16 +2,19 @@ package ch.wsl.box.client.styles
+import ch.wsl.box.client.services.ClientConf
import ch.wsl.box.client.styles.constants.StyleConstants
import ch.wsl.box.client.styles.constants.StyleConstants.{ChildProperties, Colors}
import ch.wsl.box.client.styles.fonts.Font
import ch.wsl.box.client.styles.utils.{ColorUtils, MediaQueries, StyleUtils}
+import scalacss.StyleSheet
import scala.language.postfixOps
import scalacss.internal.{AV, CanIUse, FontFace}
import scalacss.internal.CanIUse.{Agent, boxshadow}
import scalacss.internal.DslBase.ToStyle
import scalacss.internal.LengthUnit.px
+import scalacss.internal.mutable.{Register, Settings}
import scalatags.JsDom
import scalatags.generic.Attr
@@ -20,1647 +23,1683 @@ import scala.concurrent.duration.DurationInt
case class StyleConf(colors:Colors, smallCellsSize:Int, childProps: ChildProperties, requiredFontSize:Int, paddingBlocks: Int, inputPercentage:Double)
-object GlobalStyleFactory{
- val CssSettings = scalacss.devOrProdDefaults; import CssSettings._
-
+class GlobalStyleFactory extends BoxStyleFactory {
+ override def build(settings:Settings): BoxStyle = {
+ val styles = new GlobalStyles(settings,ClientConf.styleConf)
+ styles.loadGlobalStyle()
+ styles
+ }
- case class GlobalStyles(conf:StyleConf) extends StyleSheet.Inline {
+}
+class GlobalStyles(settings:Settings,conf:StyleConf) extends StyleSheet.Inline()(settings.cssRegister) with BoxStyle {
- import dsl._
+ import settings._
- val inputDefaultWidth = width(conf.inputPercentage %%)
+ import dsl._
- val inputHighlight = style(
- borderWidth(0 px,0 px,1 px,0 px),
- borderColor(conf.colors.main),
- backgroundColor(c"#f5f5f5"),
- outlineWidth.`0`
- )
+ protected val inputDefaultWidth = width(conf.inputPercentage %%)
- val inputInvalid = style(
- borderColor(conf.colors.danger),
- backgroundColor(ColorUtils.RGB.fromHex(conf.colors.dangerColor).withTrasparency(0.3))
- )
+ override val inputHighlight = style(
+ borderWidth(0 px,0 px,1 px,0 px),
+ borderColor(conf.colors.main),
+ backgroundColor(c"#f5f5f5"),
+ outlineWidth.`0`
+ )
- val global = style(
+ override val inputInvalid = style(
+ borderColor(conf.colors.danger),
+ backgroundColor(ColorUtils.RGB.fromHex(conf.colors.dangerColor).withTrasparency(0.3))
+ )
- unsafeRoot(".col, .col-1, .col-10, .col-11, .col-12, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-auto, .col-lg, .col-lg-1, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-auto, .col-md, .col-md-1, .col-md-10, .col-md-11, .col-md-12, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-auto, .col-sm, .col-sm-1, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-auto, .col-xl, .col-xl-1, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-auto") (
- padding.`0`
- ),
- unsafeRoot(".row")(
- margin.`0`
- ),
- unsafeRoot("b") (
- Font.bold
- ),
+ def loadGlobalStyle() = style(
- unsafeRoot("h4")(
- marginTop(20 px)
- ),
+ unsafeRoot(".col, .col-1, .col-10, .col-11, .col-12, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-auto, .col-lg, .col-lg-1, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-auto, .col-md, .col-md-1, .col-md-10, .col-md-11, .col-md-12, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-auto, .col-sm, .col-sm-1, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-auto, .col-xl, .col-xl-1, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-auto") (
+ padding.`0`
+ ),
- unsafeRoot(".block-el-0 h4")(
- marginTop.`0`.important
- ),
+ unsafeRoot(".row")(
+ margin.`0`
+ ),
- unsafeRoot("h5")(
- Font.bold,
- fontSize(14 px),
- media.maxWidth(600 px)( //disable autozoom
- fontSize(16 px)
- )
- ),
+ unsafeRoot("b") (
+ Font.bold
+ ),
- unsafeRoot("body") (
- StyleConstants.defaultFontSize,
- backgroundColor.white,
- Font.regular
- ),
+ unsafeRoot("h4")(
+ marginTop(20 px)
+ ),
- unsafeRoot("html, body") (
- touchAction := "pan-x pan-y"
- ),
+ unsafeRoot(".block-el-0 h4")(
+ marginTop.`0`.important
+ ),
- unsafeRoot("h3") (
- marginTop(18 px)
- ),
+ unsafeRoot("h5")(
+ Font.bold,
+ fontSize(14 px),
+ media.maxWidth(600 px)( //disable autozoom
+ fontSize(16 px)
+ )
+ ),
- unsafeRoot("*:focus")(
- outline.none
- ),
+ unsafeRoot("body") (
+ StyleConstants.defaultFontSize,
+ backgroundColor.white,
+ Font.regular
+ ),
- unsafeRoot("select")(
- inputDefaultWidth,
- media.maxWidth(600 px)(
- width(100 %%)
- ),
- borderStyle.solid,
- borderWidth(0 px,0 px,1 px,0 px),
- borderRadius.`0`,
- borderColor(Colors.GreySemi),
- height(23 px),
- backgroundColor.transparent,
- Font.regular,
- &.focus(
- inputHighlight
- ),
- &.hover(
- inputHighlight
- ),
- &.invalid(
- inputInvalid
- ),
- media.maxWidth(600 px)( //disable autozoom
- height(26 px),
- fontSize(16 px)
- )
- ),
+ unsafeRoot("html, body") (
+ touchAction := "pan-x pan-y"
+ ),
- unsafeRoot("input")(
- inputDefaultWidth,
- media.maxWidth(600 px)(
- width(100 %%)
- ),
- borderStyle.solid,
- borderWidth(0 px,0 px,1 px,0 px),
- borderRadius.`0`,
- backgroundColor.white,
- borderColor(Colors.GreySemi),
- height(23 px),
- Font.regular,
- paddingLeft(5 px),
- paddingRight(5 px),
- backgroundColor.transparent,
- &.focus(
- inputHighlight
- ),
- &.hover(
- inputHighlight
- ),
- &.invalid(
- inputInvalid
- ),
- media.maxWidth(600 px)( //disable autozoom
- height(26 px),
- fontSize(16 px)
- )
- ),
+ unsafeRoot("h3") (
+ marginTop(18 px)
+ ),
+ unsafeRoot("*:focus")(
+ outline.none
+ ),
- unsafeRoot("label")(
- Font.bold
+ unsafeRoot("select")(
+ inputDefaultWidth,
+ media.maxWidth(600 px)(
+ width(100 %%)
),
-
- unsafeRoot("input[type='checkbox']")(
- width.auto,
- height.auto
+ borderStyle.solid,
+ borderWidth(0 px,0 px,1 px,0 px),
+ borderRadius.`0`,
+ borderColor(Colors.GreySemi),
+ height(23 px),
+ backgroundColor.transparent,
+ Font.regular,
+ &.focus(
+ inputHighlight
),
-
- unsafeRoot("input[type='number']")(
- textAlign.right
+ &.hover(
+ inputHighlight
),
-
- unsafeRoot(".flatpickr-time input[type='number']")(
- textAlign.center
+ &.invalid(
+ inputInvalid
),
+ media.maxWidth(600 px)( //disable autozoom
+ height(26 px),
+ fontSize(16 px)
+ )
+ ),
- unsafeRoot("input[type='file']")(
- width(100 %%),
- height.auto,
- borderWidth(0 px),
- backgroundColor.transparent
+ unsafeRoot("input")(
+ inputDefaultWidth,
+ media.maxWidth(600 px)(
+ width(100 %%)
),
-
- unsafeRoot("textarea")(
- width(100 %%),
- borderStyle.solid,
- borderWidth(1 px),
- borderRadius.`0`,
- borderLeft.`0`,
- borderRight.`0`,
- borderTop.`0`,
- backgroundColor.transparent,
- borderColor(Colors.GreySemi),
- resize.vertical,
- overflowY.auto,
- wordWrap.breakWord,
- minHeight(30 px),
- maxHeight(200 px),
- media.maxWidth(600 px)( //disable autozoom
- fontSize(16 px)
- ),
- transition := "all .1s linear;",
- &.focus(
- inputHighlight
- ),
- &.hover(
- inputHighlight
- ),
+ borderStyle.solid,
+ borderWidth(0 px,0 px,1 px,0 px),
+ borderRadius.`0`,
+ backgroundColor.white,
+ borderColor(Colors.GreySemi),
+ height(23 px),
+ Font.regular,
+ paddingLeft(5 px),
+ paddingRight(5 px),
+ backgroundColor.transparent,
+ &.focus(
+ inputHighlight
+ ),
+ &.hover(
+ inputHighlight
),
+ &.invalid(
+ inputInvalid
+ ),
+ media.maxWidth(600 px)( //disable autozoom
+ height(26 px),
+ fontSize(16 px)
+ )
+ ),
- unsafeRoot("option")(
- direction.ltr
- ),
+ unsafeRoot("label")(
+ Font.bold
+ ),
- unsafeRoot("header")(
- clear.both,
- height(50 px),
- padding(10 px, 20 px, 10 px, 20 px),
- media.minWidth(600 px)(
- paddingLeft(50 px)
- ),
- lineHeight(29 px),
- border.`0`,
- color(conf.colors.mainText),
- backgroundColor(conf.colors.main),
- fontSize(0.8.rem),
- unsafeChild("ul") (
- display.inline,
- margin(10 px)
- ),
- unsafeChild("li") (
- display.inline,
- margin(10 px)
- ),
- position.sticky,
- zIndex(100),
- boxShadow := "0px 0px 3px #555"
- ),
+ unsafeRoot("input[type='checkbox']")(
+ width.auto,
+ height.auto
+ ),
- unsafeRoot("footer")(
- borderTop(conf.colors.main,5 px,solid),
- backgroundColor.white,
- overflow.hidden,
- fontSize(11 px),
- color.darkgray,
- padding(15 px),
- height(55 px)
- ),
+ unsafeRoot("input[type='number']")(
+ textAlign.right
+ ),
- unsafeRoot(".form-control")( // this controls the datetime input
- paddingTop(1 px),
- paddingBottom(1 px),
- paddingRight(5 px),
- textAlign.left,
- lineHeight(14 px),
- height(21 px),
- borderStyle.solid,
- borderWidth(1 px),
- borderRadius.`0`,
- backgroundColor.white,
- borderColor(Colors.GreySemi)
- ),
+ unsafeRoot(".flatpickr-time input[type='number']")(
+ textAlign.center
+ ),
- unsafeRoot(".modal") (
- backgroundColor.rgba(0,0,0,0.5),
- ),
+ unsafeRoot("input[type='file']")(
+ width(100 %%),
+ height.auto,
+ borderWidth(0 px),
+ backgroundColor.transparent
+ ),
- unsafeRoot(".modal-sm") (
- maxWidth(800 px).important
+ unsafeRoot("textarea")(
+ width(100 %%),
+ borderStyle.solid,
+ borderWidth(1 px),
+ borderRadius.`0`,
+ borderLeft.`0`,
+ borderRight.`0`,
+ borderTop.`0`,
+ backgroundColor.transparent,
+ borderColor(Colors.GreySemi),
+ resize.vertical,
+ overflowY.auto,
+ wordWrap.breakWord,
+ minHeight(30 px),
+ maxHeight(200 px),
+ media.maxWidth(600 px)( //disable autozoom
+ fontSize(16 px)
),
- unsafeRoot(".modal-lg") (
- maxWidth(90 %%).important,
- media.maxWidth(600 px)(
- maxWidth(100 %%).important,
- ),
+ transition := "all .1s linear;",
+ &.focus(
+ inputHighlight
+ ),
+ &.hover(
+ inputHighlight
),
+ ),
- unsafeRoot(".rotate-right") (
- transform := "rotate(90deg)"
- ),
+ unsafeRoot("option")(
+ direction.ltr
+ ),
- unsafeRoot(".rotate-left") (
- transform := "rotate(-90deg)"
+ unsafeRoot("header")(
+ clear.both,
+ height(50 px),
+ padding(10 px, 20 px, 10 px, 20 px),
+ media.minWidth(600 px)(
+ paddingLeft(50 px)
),
-
- unsafeRoot(".container-fluid")(
- padding.`0`
+ lineHeight(29 px),
+ border.`0`,
+ color(conf.colors.mainText),
+ backgroundColor(conf.colors.main),
+ fontSize(0.8.rem),
+ unsafeChild("ul") (
+ display.inline,
+ margin(10 px)
+ ),
+ unsafeChild("li") (
+ display.inline,
+ margin(10 px)
),
+ position.sticky,
+ zIndex(100),
+ boxShadow := "0px 0px 3px #555"
+ ),
- unsafeRoot("main")(
-// media.minWidth(600 px)(
-// paddingLeft(50 px),
-// paddingRight(50 px)
-// ),
- backgroundColor.white
+ unsafeRoot("footer")(
+ borderTop(conf.colors.main,5 px,solid),
+ backgroundColor.white,
+ overflow.hidden,
+ fontSize(11 px),
+ color.darkgray,
+ padding(15 px),
+ height(55 px)
+ ),
+
+ unsafeRoot(".action") (
+ cursor.pointer
+ ),
+
+ unsafeRoot(".nav-item") (
+ cursor.pointer
+ ),
+
+ unsafeRoot(".form-control")( // this controls the datetime input
+ paddingTop(1 px),
+ paddingBottom(1 px),
+ paddingRight(5 px),
+ textAlign.left,
+ lineHeight(14 px),
+ height(21 px),
+ borderStyle.solid,
+ borderWidth(1 px),
+ borderRadius.`0`,
+ backgroundColor.white,
+ borderColor(Colors.GreySemi)
+ ),
+
+ unsafeRoot(".modal") (
+ backgroundColor.rgba(0,0,0,0.5),
+ ),
+
+ unsafeRoot(".modal-sm") (
+ maxWidth(800 px).important
+ ),
+ unsafeRoot(".modal-lg") (
+ maxWidth(90 %%).important,
+ media.maxWidth(600 px)(
+ maxWidth(100 %%).important,
),
+ ),
- unsafeRoot("a")(
- &.hover(
- textDecorationLine.underline,
- color(conf.colors.mainLink)
- ),
- color(conf.colors.mainLink),
- cursor.pointer,
- ),
+ unsafeRoot(".rotate-right") (
+ transform := "rotate(90deg)"
+ ),
- unsafeRoot("main a") (
- media.maxWidth(600 px)(
- display.inlineBlock,
- margin.vertical(5 px)
- ),
- ),
+ unsafeRoot(".rotate-left") (
+ transform := "rotate(-90deg)"
+ ),
+ unsafeRoot(".container-fluid")(
+ padding.`0`
+ ),
+
+ unsafeRoot("main")(
+ // media.minWidth(600 px)(
+ // paddingLeft(50 px),
+ // paddingRight(50 px)
+ // ),
+ backgroundColor.white
+ ),
- unsafeRoot("#box-table table")(
- backgroundColor.white
+ unsafeRoot("a")(
+ &.hover(
+ textDecorationLine.underline,
+ color(conf.colors.mainLink)
),
+ color(conf.colors.mainLink),
+ cursor.pointer,
+ ),
- //hide up/down arrow for input
- unsafeRoot("input[type=\"number\"]::-webkit-outer-spin-button,\n input[type=\"number\"]::-webkit-inner-spin-button")(
- StyleUtils.unsafeProp("-webkit-appearance","none"),
- margin.`0`
+ unsafeRoot("main a") (
+ media.maxWidth(600 px)(
+ display.inlineBlock,
+ margin.vertical(5 px)
),
+ ),
- unsafeRoot("input[type=\"number\"]")(
- StyleUtils.unsafeProp("-moz-appearance","textfield")
- ),
+ unsafeRoot("#box-table table")(
+ backgroundColor.white
+ ),
- unsafeRoot(".ql-editor")(
- fontWeight.normal
- ),
- unsafeRoot(".ql-editor p")(
- marginBottom.`0`
- ),
+ //hide up/down arrow for input
+ unsafeRoot("input[type=\"number\"]::-webkit-outer-spin-button,\n input[type=\"number\"]::-webkit-inner-spin-button")(
+ StyleUtils.unsafeProp("-webkit-appearance","none"),
+ margin.`0`
+ ),
- unsafeRoot (".field")(
- width(100 %%)
- ),
+ unsafeRoot("input[type=\"number\"]")(
+ StyleUtils.unsafeProp("-moz-appearance","textfield")
+ ),
- unsafeRoot(".grid-stack > .grid-stack-item.grid-stack-sub-grid > .grid-stack-item-content") (
- backgroundColor.rgba(0,0,0,0.1),
- marginRight(2.px)
- )
+ unsafeRoot(".ql-editor")(
+ fontWeight.normal
+ ),
- )
+ unsafeRoot(".ql-editor p")(
+ marginBottom.`0`
+ ),
- val spaceBetween = style(
- display.flex,
- flexDirection.column,
- media.minWidth(600 px)(
- flexDirection.row,
- ),
- justifyContent.spaceBetween,
- alignItems.center,
- alignContent.center
+ unsafeRoot (".field")(
+ width(100 %%)
+ ),
+
+ unsafeRoot(".grid-stack > .grid-stack-item.grid-stack-sub-grid > .grid-stack-item-content") (
+ backgroundColor.rgba(0,0,0,0.1),
+ marginRight(2.px)
)
- val flexContainer = style(
- display.flex,
+
+ )
+
+ override val spaceBetween = style(
+ display.flex,
+ flexDirection.column,
+ media.minWidth(600 px)(
flexDirection.row,
- height(100 %%),
+ ),
+ justifyContent.spaceBetween,
+ alignItems.center,
+ alignContent.center
+ )
+
+ override val flexContainer = style(
+ display.flex,
+ flexDirection.row,
+ height(100 %%),
+ width(100 %%),
+
+ )
+
+ override val sidebarRightContent = style(
+ flex := "1 0 auto",
+ width(100 %%),
+ transitionDuration(300 millis),
+ unsafeExt(_ + ".showSidebar")(
+ media.minWidth(600 px)(
+ width :=! "calc(100% - 250px)",
+ transitionDuration(300 millis),
+ )
+ ),
+ )
+
+ override val sidebar = style(
+ marginLeft(-250 px),
+ height :=! "calc(100vh - 250px)",
+ overflowX.hidden,
+ transitionDuration(300 millis),
+ width(250 px),
+ paddingRight(80 px),
+ unsafeExt(_ + ".showSidebar")(
+ marginLeft.`0`,
+ paddingRight(30 px),
+ transitionDuration(300 millis),
+ ),
+ unsafeChild("input") (
width(100 %%),
+ ),
+ unsafeChild("li") (
+ lineHeight(22 px)
+ ),
+ unsafeChild("ul") (
+ marginTop(20 px)
+ ),
+ media.maxWidth(600 px)(
+ display.none,
+ ),
+
+ )
+
+
+ override val spaceAfter = style(
+ display.flex,
+ flexDirection.column,
+ media.minWidth(600 px)(
+ flexDirection.row,
+ ),
+ justifyContent.start,
+ alignItems.center,
+ alignContent.center
+ )
+
+ override val navigationBlock = style(
+ display.flex,
+ flexDirection.row,
+ justifyContent.spaceBetween,
+ alignItems.center,
+ alignContent.center,
+ media.maxWidth(600 px)(
+ width(90 %%)
+ )
+ )
+
+ override val textNoWrap = style(
+ whiteSpace.nowrap
+ )
+
+ override val dataChanged = style(
+ color(conf.colors.danger),
+ fontSize(14 px)
+ )
+
+ override val formTitle = style(
+ fontWeight.bold,
+ fontSize(18 px),
+ padding(0.75 rem)
+ )
+
+ override val formTitleLight = style(
+ fontWeight._300,
+ fontSize(14 px)
+ )
+
+ override val chipLink = style(
+ borderColor.gray,
+ padding(5 px, 10 px),
+ marginLeft(10 px),
+ fontSize(10 px),
+ backgroundColor(Color("#eee")),
+ borderRadius(20 px),
+ &.hover(
+ color(conf.colors.main).important,
+ backgroundColor(ColorUtils.RGB.fromHex(conf.colors.mainColor).withTrasparency(0.4))
+ )
+ )
+
+ override val chip = style(
+ color(conf.colors.main).important,
+ backgroundColor(ColorUtils.RGB.fromHex(conf.colors.mainColor).withTrasparency(0.4)),
+ padding(5 px, 10 px),
+ marginLeft(10 px),
+ fontSize(10 px),
+ borderRadius(20 px),
+ textNoWrap
+ )
+
+ override val checkboxWidget = style(
+ float.none.important,
+ marginRight(5.px)
+ )
+
+ override val jsonMetadataRendered = style(
+ unsafeChild("input") (
+ float.right
+ ),
+ unsafeChild("textarea") (
+ float.right
+ ),
+ unsafeChild(".jexcel input") (
+ float.none
)
+ )
- val sidebarRightContent = style(
- flex := "1 0 auto",
- width(100 %%),
- transitionDuration(300 millis),
- unsafeExt(_ + ".showSidebar")(
- media.minWidth(600 px)(
- width :=! "calc(100% - 250px)",
- transitionDuration(300 millis),
+ override val preformatted = style(
+ whiteSpace.preLine
+ )
+
+ override val formHeader = style(
+ margin(`0`, 10 px)
+ )
+
+ override val dateTimePicker = style(
+ inputDefaultWidth,
+ media.maxWidth(600 px)(
+ width(100 %%)
+ ),
+ textAlign.right,
+ float.right
+ )
+
+ override val dateTimePickerFullWidth = style(
+ width(100 %%),
+ textAlign.right,
+ float.right
+ )
+
+ override val tableHeaderFixed = style(
+ unsafeChild("table")(
+ verticalAlign.middle,
+ height.fitContent
+ ),
+ unsafeChild("thead") (
+ position.sticky,
+ top.`0`,
+ backgroundColor.white,
+ unsafeChild("td") (
+ border.`0`,
+ media.maxWidth(600 px)(
+ padding.vertical(5 px)
)
- ),
+ )
)
+ )
- val sidebar = style(
- marginLeft(-250 px),
- height :=! "calc(100vh - 250px)",
- overflowX.hidden,
- transitionDuration(300 millis),
- width(250 px),
- paddingRight(80 px),
- unsafeExt(_ + ".showSidebar")(
- marginLeft.`0`,
- paddingRight(30 px),
- transitionDuration(300 millis),
+ override val smallCells = style(
+ padding.horizontal(3 px),
+ padding.vertical(15 px),
+ verticalAlign.middle.important,
+ fontSize(conf.smallCellsSize px),
+ unsafeRoot("input") (
+ media.maxWidth(600 px)(
+ height(22 px),
+ fontSize(14 px)
+ ),
+ ),
+ unsafeRoot("p")(
+ margin(0 px)
+ )
+ )
+
+ override val tableCellActions = style(
+ height(100 %%),
+ minWidth(80 px),
+ fontSize(15 px),
+ display.flex,
+ alignItems.center,
+ justifyContent.spaceAround,
+ unsafeChild("a.action.primary") (
+ &.hover(
+ color(conf.colors.main)
+ )
+ ),
+ unsafeChild("a.action.danger") (
+ &.hover(
+ color(conf.colors.danger)
+ )
+ ),
+ // unsafeChild("svg") (
+ // transform := "scale(1.5)"
+ // )
+ )
+
+ override val rowStyle = style(
+ height(100 %%),
+ unsafeChild("a.action") (
+ color(Color("#ccc")),
+ fontSize(11 px)
+ ),
+ borderLeft(4 px,solid,transparent),
+
+ unsafeExt(_ + ".selected") (
+ unsafeChild("a.action") (
+ color(Color("#333")),
),
- unsafeChild("input") (
- width(100 %%),
+ backgroundColor(ColorUtils.RGB.fromHex(conf.colors.mainColor).withTrasparency(0.2))
+ ),
+ &.hover(
+ backgroundColor(rgba(250,248,250,1)),
+ borderLeft(4 px,solid,black),
+ unsafeChild("a.action") (
+ color(Color("#333")),
),
- unsafeChild("li") (
- lineHeight(22 px)
+ unsafeExt(_ + ".selected") (
+ backgroundColor(ColorUtils.RGB.fromHex(conf.colors.mainColor).withTrasparency(0.2))
),
- unsafeChild("ul") (
- marginTop(20 px)
+ ),
+ unsafeExt(_ + "." + StyleConstants.mapHoverClass)(
+ backgroundColor(ColorUtils.RGB.fromHex(conf.colors.mainColor).lighten(0.8).color)
+ )
+ )
+
+ override val tableHeader = style(
+ fontSize(14 px),
+ media.maxWidth(600 px)(
+ fontSize(12 px),
+ ),
+ Font.bold
+ )
+
+
+
+ override val numberCells = style(
+ textAlign.right,
+ paddingRight(3 px)
+ )
+
+ override val textCells = style(
+ textAlign.left,
+ paddingLeft(3 px)
+ )
+
+ override val lookupCells = style(
+ textAlign.center
+ )
+
+ override val dateCells = style(
+ textAlign.center
+ )
+
+ override val noPadding = style( padding.`0` )
+ override val smallBottomMargin = style( marginBottom(5 px) )
+ override val mediumBottomMargin = style( marginBottom(15 px) )
+
+ override val subBlock = style(
+ padding(conf.paddingBlocks px).important,
+ minHeight.`0`
+ )
+
+ override val block = style(
+ paddingTop.`0`,
+ paddingBottom.`0`,
+ minHeight.`0`
+ )
+
+ override val innerBlock = style(
+ margin.`0`
+ )
+
+ override val withBorder = style(
+ borderBottom(conf.childProps.borderSize px,solid, conf.childProps.borderColor),
+ paddingBottom(5 px),
+ paddingTop(5 px)
+ )
+
+ override val removeFlexChild = style(
+ borderLeftColor.transparent,
+ borderLeftStyle.solid,
+ borderLeftWidth(3 px),
+ &.hover(
+ borderLeftColor(conf.colors.main)
+ )
+ )
+
+ override val tableContainer = style(
+ paddingLeft(10.px),
+ paddingRight(10.px),
+ paddingBottom(20.px)
+ )
+
+ override val table = style(
+ borderColor(Colors.GreySemi),
+ borderCollapse.collapse,
+ unsafeChild("th") (
+ Font.bold,
+ //borderColor(conf.colors.main),
+ ),
+ unsafeChild("td") (
+ //borderColor(conf.colors.main),
+ //borderStyle.solid,
+ //borderWidth(1 px),
+ paddingLeft(10 px),
+ paddingRight(10 px),
+ paddingTop(10 px),
+ paddingBottom(10 px),
+ media.maxWidth(600 px)(
+ verticalAlign.textTop
),
+ ),
+ unsafeChild("label") (
+ marginBottom.`0`,
media.maxWidth(600 px)(
- display.none,
+ width(100 %%),
+ justifyContent.spaceEvenly
),
+ ),
+ width(100 %%)
+ )
+ override val field = style(
+ paddingRight(10 px),
+ paddingLeft(10 px),
+ minHeight.`0`,
+ media.maxWidth(600 px)( //disable autozoom
+ paddingTop(10 px)
)
+ )
-
-
- val spaceAfter = style(
- display.flex,
- flexDirection.column,
- media.minWidth(600 px)(
- flexDirection.row,
- ),
- justifyContent.start,
- alignItems.center,
- alignContent.center
+ override val fieldHighlight = style(
+ unsafeExt(_ + ":focus-within")(
+ unsafeChild("label")(
+ color(conf.colors.main),
+ )
)
-
- val navigationBlock = style(
- display.flex,
- flexDirection.row,
- justifyContent.spaceBetween,
- alignItems.center,
- alignContent.center,
- media.maxWidth(600 px)(
- width(90 %%)
+ )
+
+ override val removeFieldMargin = style(
+ marginRight(-10 px),
+ marginLeft(-10 px),
+ minHeight.`0`
+ )
+
+ override val removeFieldAndBlockMargin = style(
+ marginRight((-10-conf.paddingBlocks) px),
+ marginLeft((-10-conf.paddingBlocks) px),
+ minHeight.`0`
+ )
+
+ override val distributionContrainer = style(
+ display.flex,
+ flexDirection.row,
+ flexWrap.wrap,
+ justifyContent.start,
+ alignItems.center,
+ alignContent.spaceAround
+ )
+
+ override val distributionChild = style(
+ padding(10.px),
+ margin(5.px)
+ )
+
+ override val boxedLink = style(
+ Font.bold,
+ width(120 px),
+ height(120 px),
+ padding(20 px),
+ margin(20 px)
+ )
+
+
+ override val notificationArea = style(
+ position.fixed,
+ top(40 px),
+ right(40 px),
+ zIndex(2000)
+ )
+
+ override val notification = style(
+ padding(20 px),
+ border(1 px,solid,red),
+ backgroundColor.white
+ )
+
+ override val headerLogo = style(
+ height(40 px),
+ maxWidth( 100 %%),
+ marginTop(-10 px),
+ marginBottom(-5 px),
+ marginLeft(0 px),
+ marginRight(10 px)
+ )
+
+ override val headerTitle = style(
+ fontSize(20 px),
+ //color.rgba(255,255,255,0.8),
+ //text-shadow: 0px 3px 3px #006268, 0 0px 0px #fff, 0px 3px 3px #006268;
+ )
+
+ override val linkHeaderFooter = style(
+ textDecorationLine.none.important,
+ unsafeChild("span") (
+ textDecorationLine.none.important,
+ &.hover(
+ textDecorationLine.none.important,
+ color(conf.colors.link)
)
+ ),
+ &.hover(
+ textDecorationLine.none.important,
+ color(conf.colors.link)
+ ),
+ color(conf.colors.link),
+ textTransform.uppercase,
+ Font.bold,
+ cursor.pointer,
+ padding.horizontal(2.px)
+ )
+
+
+
+ override val fullHeightMax = style(
+ height :=! "calc(100vh - 150px)",
+ media.maxWidth(600 px)(
+ height :=! "calc(100vh - 110px)",
+ //paddingBottom(70 px)
+ ),
+ overflow.auto
+ )
+
+ override val tableWrapper = style(
+ addClassName("box-table-wrapper")
+ )
+
+ override val fullHeight = style(
+ height :=! "calc(100vh - 105px)",
+ media.maxWidth(600 px)(
+ height :=! "calc(100vh - 53px)",
+ ),
+ overflow.auto,
+ width(100.%%)
+ )
+
+ override val loading = style(
+ position.fixed,
+ top.`0`,
+ left.`0`,
+ width(100.%%),
+ height(100.%%),
+ backgroundColor(rgba(0,0,0,0.5)),
+ paddingTop(200 px),
+ textAlign.center,
+ color.white,
+ fontSize(20 px),
+ zIndex(9999)
+ )
+
+ override val noMargin = style(
+ margin.`0`
+ )
+
+ override val subform = style(
+ marginTop(conf.childProps.marginTopSize px),
+ padding(conf.childProps.paddingSize px),
+ border(conf.childProps.borderSize px,solid, conf.childProps.borderColor),
+ backgroundColor(conf.childProps.backgroundColor),
+ overflow.hidden
+ )
+
+ override val childTable = style(
+ backgroundColor(conf.childProps.backgroundColor),
+ border(conf.childProps.borderSize px,solid, conf.childProps.borderColor),
+ width(100.%%),
+ overflow.hidden
+ )
+
+ override val childTableTr = style(
+ border(conf.childProps.borderSize px,solid, conf.childProps.borderColor),
+ borderBottom.`0`
+ )
+
+ override val childTableTd = style(
+ fontSize(conf.smallCellsSize px),
+ padding(5.px)
+ )
+
+ override val childTableHeader = style(
+ Font.bold
+ )
+
+ override val childTableAction = style(
+ width(10.px)
+ )
+
+ override val childFormTableTr = style(
+ borderTop.`0`
+ )
+
+ override val childFormTableTd = style(
+ StyleConstants.defaultFontSize
+ )
+
+ override val boxIconButton = style(
+ Font.regular,
+ whiteSpace.nowrap,
+ padding(3 px, 10 px),
+ fontSize(16 px),
+ margin(3 px, 0 px),
+ border(0 px),
+ color(conf.colors.mainLink),
+ backgroundColor.transparent,
+ unsafeChild("svg") (
+ transform := "scale(1.5)"
+ ),
+ &.hover(
+ color(white),
+ backgroundColor(conf.colors.main)
+ ),
+ &.attrExists("disabled") (
+ backgroundColor(lightgray),
+ color(gray),
+ borderColor(gray)
+ )
+ )
+
+ override val boxIconButtonDanger = style(
+ Font.regular,
+ whiteSpace.nowrap,
+ padding(7 px, 15 px),
+ fontSize(16 px),
+ margin(3 px, 0 px),
+ border(0 px),
+ color(conf.colors.danger),
+ backgroundColor.transparent,
+ unsafeChild("svg") (
+ transform := "scale(1.5)"
+ ),
+ &.hover(
+ color(white),
+ backgroundColor(conf.colors.danger)
+ ),
+ &.attrExists("disabled") (
+ backgroundColor(lightgray),
+ color(gray),
+ borderColor(gray)
+ )
+ )
+
+
+ override val boxButton = style(
+ Font.regular,
+ whiteSpace.nowrap,
+ height.auto,
+ padding(7 px, 15 px),
+ fontSize(14 px),
+ minWidth(25 px),
+ textAlign.center,
+ lineHeight(16 px),
+ margin(9 px, 5 px, 9 px, 0 px),
+ border.`0`,
+ color(conf.colors.mainLink),
+ backgroundColor(white),
+ cursor.pointer,
+ &.hover(
+ color(white),
+ backgroundColor(conf.colors.main),
+ opacity(0.5),
+ ),
+ &.attrExists("disabled") (
+ backgroundColor.transparent,
+ color(gray),
+ cursor.default
+ //borderColor(gray)
)
+ )
- val textNoWrap = style(
- whiteSpace.nowrap
- )
+ override val boxNavigationLabel = style(
+ textAlign.center,
+ lineHeight(26 px),
+ fontSize(12 px)
+ )
- val dataChanged = style(
- color(conf.colors.danger),
- fontSize(14 px)
+ override val spacedList = style(
+ unsafeChild("li") (
+ marginTop(10 px),
+ marginBottom(10 px)
)
+ )
- val formTitle = style(
- fontWeight.bold,
- fontSize(18 px),
- padding(0.75 rem)
- )
- val formTitleLight = style(
- fontWeight._300,
- fontSize(14 px)
- )
- val chipLink = style(
- borderColor.gray,
- padding(5 px, 10 px),
- marginLeft(10 px),
- fontSize(10 px),
- backgroundColor(Color("#eee")),
- borderRadius(20 px),
- &.hover(
- color(conf.colors.main).important,
- backgroundColor(ColorUtils.RGB.fromHex(conf.colors.mainColor).withTrasparency(0.4))
- )
+ override val boxButtonImportant = style(
+ Font.regular,
+ whiteSpace.nowrap,
+ height.auto,
+ padding(7 px, 15 px),
+ fontSize(14 px),
+ lineHeight(16 px),
+ margin(9 px, 5 px, 9 px, 0 px),
+ border.`0`,
+ boxShadow := "0px 0px 2px #555",
+ backgroundColor(conf.colors.main),
+ color(conf.colors.mainText),
+ &.hover(
+ backgroundColor.white,
+ color(conf.colors.mainLink),
+ borderColor(conf.colors.mainLink),
+ boxShadow := "0px 0px 5px #555"
+ )
+ )
+
+ override val boxButtonDanger = style(
+ Font.regular,
+ whiteSpace.nowrap,
+ height.auto,
+ padding(7 px, 15 px),
+ fontSize(14 px),
+ lineHeight(16 px),
+ margin(9 px, 5 px, 9 px, 0 px),
+ border.`0`,
+ boxShadow := "0px 0px 2px #555",
+ backgroundColor(conf.colors.danger),
+ color.white,
+ &.hover(
+ backgroundColor.white,
+ color(conf.colors.danger)
)
+ )
- val chip = style(
- color(conf.colors.main).important,
- backgroundColor(ColorUtils.RGB.fromHex(conf.colors.mainColor).withTrasparency(0.4)),
- padding(5 px, 10 px),
- marginLeft(10 px),
- fontSize(10 px),
- borderRadius(20 px),
- textNoWrap
- )
+ override val popupButton = style(
+ inputDefaultWidth,
+ media.maxWidth(600 px)(
+ width(100 %%)
+ ),
+ borderStyle.solid,
+ borderWidth(0 px,0 px,1 px,0 px),
+ borderRadius.`0`,
+ borderColor(Colors.GreySemi),
+ minHeight(23 px),
+ backgroundColor.transparent,
+ cursor.pointer,
+ &.focus(
+ inputHighlight
+ ),
+ &.hover(
+ inputHighlight
+ )
+ )
+
+ override val popupEntiresList = style(
+ overflowY.auto,
+ maxHeight(70.vh)
+ )
+ override val popupEntiresItem = style(
+ display.flex,
+ justifyContent.spaceBetween,
+ marginBottom(10 px)
+ )
+
+ override val fullWidth = style(
+ width(100 %%).important
+ )
+
+ override val maxFullWidth = style(
+ maxWidth(100 %%).important
+ )
+
+ override val filterTableSelect = style(
+ border.`0`,
+ media.maxWidth(600 px)(
+ height(20 px),
+ fontSize(11 px)
+ ),
+ )
- val checkboxWidget = style(
- float.none.important,
- marginRight(5.px)
- )
- val jsonMetadataRendered = style(
- unsafeChild("input") (
- float.right
- ),
- unsafeChild("textarea") (
- float.right
- ),
- unsafeChild(".jexcel input") (
- float.none
- )
- )
- val preformatted = style(
- whiteSpace.preLine
- )
+ override val imageThumb = style(
+ height.auto,
+ maxWidth(100 %%)
+ )
- val formHeader = style(
- margin(`0`, 10 px)
- )
+ override val noBullet = style(
+ listStyleType := "none",
+ paddingLeft.`0`,
+ fontSize(12 px)
+ )
- val dateTimePicker = style(
- inputDefaultWidth,
- media.maxWidth(600 px)(
- width(100 %%)
- ),
- textAlign.right,
- float.right
- )
+ val navigationArea = style(
+ paddingLeft(20 px),
+ paddingRight(20 px),
+ paddingTop(5 px),
+ paddingBottom(5 px)
+ )
- val dateTimePickerFullWidth = style(
- width(100 %%),
- textAlign.right,
- float.right
- )
+ val navigatorArea = style(
+ width(260 px),
+ textAlign.right
+ )
- val tableHeaderFixed = style(
- unsafeChild("table")(
- verticalAlign.middle,
- height.fitContent
- ),
- unsafeChild("thead") (
- position.sticky,
- top.`0`,
- backgroundColor.white,
- unsafeChild("td") (
- border.`0`,
- media.maxWidth(600 px)(
- padding.vertical(5 px)
- )
- )
- )
+ val noMobile = style(
+ media.maxWidth(600 px)(
+ display.none
)
+ )
- val smallCells = style(
- padding.horizontal(3 px),
- padding.vertical(15 px),
- verticalAlign.middle.important,
- fontSize(conf.smallCellsSize px),
- unsafeRoot("input") (
- media.maxWidth(600 px)(
- height(22 px),
- fontSize(14 px)
- ),
- ),
- unsafeRoot("p")(
- margin(0 px)
- )
+ val showFooterActionOnMobile = style(
+ unsafeChild("#footerActions")(
+ display.block.important
)
+ )
- val tableCellActions = style(
- height(100 %%),
- minWidth(80 px),
- fontSize(15 px),
- display.flex,
- alignItems.center,
- justifyContent.spaceAround,
- unsafeChild("a.action.primary") (
- &.hover(
- color(conf.colors.main)
- )
- ),
- unsafeChild("a.action.danger") (
- &.hover(
- color(conf.colors.danger)
- )
- ),
-// unsafeChild("svg") (
-// transform := "scale(1.5)"
-// )
+ val mobileOnly = style(
+ display.none,
+ media.maxWidth(600 px)(
+ display.block
)
+ )
- val rowStyle = style(
- height(100 %%),
- unsafeChild("a.action") (
- color(Color("#ccc")),
- fontSize(11 px)
- ),
- borderLeft(4 px,solid,transparent),
-
- unsafeExt(_ + ".selected") (
- unsafeChild("a.action") (
- color(Color("#333")),
- ),
- backgroundColor(ColorUtils.RGB.fromHex(conf.colors.mainColor).withTrasparency(0.2))
- ),
- &.hover(
- backgroundColor(rgba(250,248,250,1)),
- borderLeft(4 px,solid,black),
- unsafeChild("a.action") (
- color(Color("#333")),
- ),
- unsafeExt(_ + ".selected") (
- backgroundColor(ColorUtils.RGB.fromHex(conf.colors.mainColor).withTrasparency(0.2))
- ),
- ),
- unsafeExt(_ + "." + StyleConstants.mapHoverClass)(
- backgroundColor(ColorUtils.RGB.fromHex(conf.colors.mainColor).lighten(0.8).color)
+ val mobileFooter = style(
+ media.maxWidth(600 px)(
+ unsafeChild("button")(
+ (width :=! "calc(100vw - 20px)").important,
+ height(35 px).important
)
)
-
- val tableHeader = style(
- fontSize(14 px),
- media.maxWidth(600 px)(
- fontSize(12 px),
- ),
- Font.bold
- )
-
-
-
- val numberCells = style(
- textAlign.right,
- paddingRight(3 px)
- )
-
- val textCells = style(
- textAlign.left,
- paddingLeft(3 px)
- )
-
- val lookupCells = style(
- textAlign.center
- )
-
- val dateCells = style(
- textAlign.center
- )
-
- val noPadding = style( padding.`0` )
- val smallBottomMargin = style( marginBottom(5 px) )
- val mediumBottomMargin = style( marginBottom(15 px) )
-
- val subBlock = style(
- padding(conf.paddingBlocks px),
- minHeight.`0`
- )
-
- val block = style(
- paddingTop.`0`,
- paddingBottom.`0`,
- minHeight.`0`
- )
-
- val innerBlock = style(
- margin.`0`
- )
-
- val withBorder = style(
- borderBottom(conf.childProps.borderSize px,solid, conf.childProps.borderColor),
- paddingBottom(5 px),
- paddingTop(5 px)
- )
-
- val removeFlexChild = style(
- borderLeftColor.transparent,
- borderLeftStyle.solid,
- borderLeftWidth(3 px),
- &.hover(
- borderLeftColor(conf.colors.main)
- )
- )
-
- val tableContainer = style(
- paddingLeft(10.px),
- paddingRight(10.px),
- paddingBottom(20.px)
- )
-
- val table = style(
- borderColor(Colors.GreySemi),
- borderCollapse.collapse,
- unsafeChild("th") (
- Font.bold,
- //borderColor(conf.colors.main),
- ),
- unsafeChild("td") (
- //borderColor(conf.colors.main),
- //borderStyle.solid,
- //borderWidth(1 px),
- paddingLeft(10 px),
- paddingRight(10 px),
- paddingTop(10 px),
- paddingBottom(10 px),
- media.maxWidth(600 px)(
- verticalAlign.textTop
- ),
- ),
- unsafeChild("label") (
- marginBottom.`0`,
- media.maxWidth(600 px)(
- width(100 %%),
- justifyContent.spaceEvenly
- ),
- ),
- width(100 %%)
- )
-
- val field = style(
- paddingRight(10 px),
- paddingLeft(10 px),
- minHeight.`0`,
- media.maxWidth(600 px)( //disable autozoom
- paddingTop(10 px)
- )
- )
-
- val fieldHighlight = style(
- unsafeExt(_ + ":focus-within")(
- unsafeChild("label")(
- color(conf.colors.main),
- )
- )
- )
-
- val removeFieldMargin = style(
- marginRight(-10 px),
- marginLeft(-10 px),
- minHeight.`0`
- )
-
- val removeFieldAndBlockMargin = style(
- marginRight((-10-conf.paddingBlocks) px),
- marginLeft((-10-conf.paddingBlocks) px),
- minHeight.`0`
- )
-
- val distributionContrainer = style(
- display.flex,
- flexDirection.row,
- flexWrap.wrap,
- justifyContent.start,
- alignItems.center,
- alignContent.spaceAround
- )
-
- val distributionChild = style(
- padding(10.px),
- margin(5.px)
- )
-
- val boxedLink = style(
- Font.bold,
- width(120 px),
- height(120 px),
- padding(20 px),
- margin(20 px)
- )
-
-
- val notificationArea = style(
- position.fixed,
- top(40 px),
- right(40 px),
- zIndex(2000)
- )
-
- val notification = style(
- padding(20 px),
- border(1 px,solid,red),
- backgroundColor.white
- )
-
- val headerLogo = style(
- height(40 px),
- maxWidth( 100 %%),
- marginTop(-10 px),
- marginBottom(-5 px),
- marginLeft(0 px),
- marginRight(10 px)
- )
-
- val headerTitle = style(
- fontSize(20 px),
- //color.rgba(255,255,255,0.8),
- //text-shadow: 0px 3px 3px #006268, 0 0px 0px #fff, 0px 3px 3px #006268;
- )
-
- val linkHeaderFooter = style(
- textDecorationLine.none.important,
- unsafeChild("span") (
- textDecorationLine.none.important,
- &.hover(
- textDecorationLine.none.important,
- color(conf.colors.link)
- )
- ),
- &.hover(
- textDecorationLine.none.important,
- color(conf.colors.link)
- ),
- color(conf.colors.link),
- textTransform.uppercase,
- Font.bold,
- cursor.pointer,
- padding.horizontal(2.px)
- )
-
-
-
- val fullHeightMax = style(
- height :=! "calc(100vh - 150px)",
- media.maxWidth(600 px)(
- height :=! "calc(100vh - 110px)",
- //paddingBottom(70 px)
- ),
- overflow.auto
- )
-
- val fullHeight = style(
- height :=! "calc(100vh - 105px)",
- media.maxWidth(600 px)(
- height :=! "calc(100vh - 53px)",
- ),
- overflow.auto,
- width(100.%%)
- )
-
- val loading = style(
- position.fixed,
- top.`0`,
- left.`0`,
- width(100.%%),
- height(100.%%),
- backgroundColor(rgba(0,0,0,0.5)),
- paddingTop(200 px),
- textAlign.center,
- color.white,
- fontSize(20 px),
- zIndex(9999)
- )
-
- val noMargin = style(
- margin.`0`
- )
-
- val subform = style(
- marginTop(conf.childProps.marginTopSize px),
- padding(conf.childProps.paddingSize px),
- border(conf.childProps.borderSize px,solid, conf.childProps.borderColor),
- backgroundColor(conf.childProps.backgroundColor),
- overflow.hidden
- )
-
- val childTable = style(
- backgroundColor(conf.childProps.backgroundColor),
- border(conf.childProps.borderSize px,solid, conf.childProps.borderColor),
- width(100.%%),
- overflow.hidden
- )
-
- val childTableTr = style(
- border(conf.childProps.borderSize px,solid, conf.childProps.borderColor),
- borderBottom.`0`
- )
-
- val childTableTd = style(
- fontSize(conf.smallCellsSize px),
- padding(5.px)
- )
-
- val childTableHeader = style(
- Font.bold
- )
-
- val childTableAction = style(
- width(10.px)
- )
-
- val childFormTableTr = style(
- borderTop.`0`
- )
-
- val childFormTableTd = style(
- StyleConstants.defaultFontSize
- )
-
- val boxIconButton = style(
- Font.regular,
- whiteSpace.nowrap,
- padding(3 px, 10 px),
- fontSize(16 px),
- margin(3 px, 0 px),
- border(0 px),
- color(conf.colors.mainLink),
- backgroundColor.transparent,
- unsafeChild("svg") (
- transform := "scale(1.5)"
- ),
- &.hover(
- color(white),
- backgroundColor(conf.colors.main)
- ),
- &.attrExists("disabled") (
- backgroundColor(lightgray),
- color(gray),
- borderColor(gray)
- )
- )
-
- val boxIconButtonDanger = style(
- Font.regular,
- whiteSpace.nowrap,
- padding(7 px, 15 px),
- fontSize(16 px),
- margin(3 px, 0 px),
- border(0 px),
- color(conf.colors.danger),
- backgroundColor.transparent,
- unsafeChild("svg") (
- transform := "scale(1.5)"
- ),
- &.hover(
- color(white),
- backgroundColor(conf.colors.danger)
- ),
- &.attrExists("disabled") (
- backgroundColor(lightgray),
- color(gray),
- borderColor(gray)
- )
- )
-
-
- val boxButton = style(
- Font.regular,
- whiteSpace.nowrap,
- height.auto,
- padding(7 px, 15 px),
- fontSize(14 px),
- minWidth(25 px),
- textAlign.center,
- lineHeight(16 px),
- margin(9 px, 5 px, 9 px, 0 px),
- border.`0`,
- color(conf.colors.mainLink),
- backgroundColor(white),
- cursor.pointer,
- &.hover(
- color(white),
- backgroundColor(conf.colors.main),
- opacity(0.5),
- ),
- &.attrExists("disabled") (
- backgroundColor.transparent,
- color(gray),
- cursor.default
- //borderColor(gray)
- )
- )
-
- val boxNavigationLabel = style(
- textAlign.center,
- lineHeight(26 px),
- fontSize(12 px)
- )
-
- val spacedList = style(
- unsafeChild("li") (
- marginTop(10 px),
- marginBottom(10 px)
- )
- )
-
-
-
- val boxButtonImportant = style(
- Font.regular,
- whiteSpace.nowrap,
- height.auto,
- padding(7 px, 15 px),
- fontSize(14 px),
- lineHeight(16 px),
- margin(9 px, 5 px, 9 px, 0 px),
- border.`0`,
- boxShadow := "0px 0px 2px #555",
+ )
+
+ val mobileBoxAction = style(
+ mobileOnly,
+ boxShadow := "0px 0px 2px #555",
+ backgroundColor(conf.colors.main),
+ color(conf.colors.mainText),
+ borderRadius(50 px),
+ position.fixed,
+ right(20 px),
+ bottom(20 px),
+ height(50 px),
+ width(50 px),
+ border.`0`,
+ fontSize(20 px),
+ zIndex(5)
+ )
+
+ val adminFormEditAction = style(
+ color(conf.colors.main),
+ backgroundColor.transparent,
+ opacity(0.6),
+ &.hover(
backgroundColor(conf.colors.main),
color(conf.colors.mainText),
- &.hover(
- backgroundColor.white,
- color(conf.colors.mainLink),
- borderColor(conf.colors.mainLink),
- boxShadow := "0px 0px 5px #555"
- )
- )
-
- val boxButtonDanger = style(
- Font.regular,
- whiteSpace.nowrap,
- height.auto,
- padding(7 px, 15 px),
- fontSize(14 px),
- lineHeight(16 px),
- margin(9 px, 5 px, 9 px, 0 px),
- border.`0`,
- boxShadow := "0px 0px 2px #555",
- backgroundColor(conf.colors.danger),
- color.white,
- &.hover(
- backgroundColor.white,
- color(conf.colors.danger)
- )
- )
-
- val popupButton = style(
- inputDefaultWidth,
- media.maxWidth(600 px)(
- width(100 %%)
- ),
- borderStyle.solid,
- borderWidth(0 px,0 px,1 px,0 px),
- borderRadius.`0`,
- borderColor(Colors.GreySemi),
- minHeight(23 px),
- backgroundColor.transparent,
- cursor.pointer,
- &.focus(
- inputHighlight
- ),
- &.hover(
- inputHighlight
- )
- )
-
- val popupEntiresList = style(
- overflowY.auto,
- maxHeight(70.vh)
- )
-
- val fullWidth = style(
- width(100 %%).important
- )
-
- val maxFullWidth = style(
- maxWidth(100 %%).important
- )
-
- val filterTableSelect = style(
- border.`0`,
- media.maxWidth(600 px)(
- height(20 px),
- fontSize(11 px)
- ),
- )
-
-
-
- val imageThumb = style(
- height.auto,
- maxWidth(100 %%)
- )
-
- val noBullet = style(
- listStyleType := "none",
- paddingLeft.`0`,
- fontSize(12 px)
- )
-
- val navigationArea = style(
- paddingLeft(20 px),
- paddingRight(20 px),
- paddingTop(5 px),
- paddingBottom(5 px)
- )
-
- val navigatorArea = style(
- width(260 px),
- textAlign.right
- )
-
- val noMobile = style(
- media.maxWidth(600 px)(
- display.none
- )
- )
-
- val showFooterActionOnMobile = style(
- unsafeChild("#footerActions")(
- display.block.important
- )
- )
-
- val mobileOnly = style(
+ opacity(1),
+ ),
+ borderRadius(50 px),
+ position.absolute,
+ right(10 px),
+ marginTop(-35 px),
+ height(30 px),
+ width(30 px),
+ border.`0`,
+ fontSize(15 px),
+ zIndex(5)
+ )
+
+ val showHide = style(
+ overflow.hidden,
+ opacity(0),
+ transition := "opacity 200ms ease-in-out",
+ unsafeExt(_ + ".hide") (
display.none,
- media.maxWidth(600 px)(
- display.block
- )
- )
-
- val mobileFooter = style(
- media.maxWidth(600 px)(
- unsafeChild("button")(
- (width :=! "calc(100vw - 20px)").important,
- height(35 px).important
- )
- )
- )
-
- val mobileBoxAction = style(
- mobileOnly,
- boxShadow := "0px 0px 2px #555",
- backgroundColor(conf.colors.main),
- color(conf.colors.mainText),
- borderRadius(50 px),
- position.fixed,
- right(20 px),
- bottom(20 px),
- height(50 px),
- width(50 px),
- border.`0`,
- fontSize(20 px),
- zIndex(5)
- )
-
- val adminFormEditAction = style(
- color(conf.colors.main),
- backgroundColor.transparent,
- opacity(0.6),
- &.hover(
- backgroundColor(conf.colors.main),
- color(conf.colors.mainText),
- opacity(1),
- ),
- borderRadius(50 px),
- position.absolute,
- right(10 px),
- marginTop(-35 px),
- height(30 px),
- width(30 px),
- border.`0`,
- fontSize(15 px),
- zIndex(5)
- )
-
- val showHide = style(
- overflow.hidden,
- opacity(0),
+ transition := "opacity 0ms",
+ ),
+ unsafeExt(_ + ".show") (
+ opacity(1),
transition := "opacity 200ms ease-in-out",
- unsafeExt(_ + ".hide") (
- display.none,
- transition := "opacity 0ms",
- ),
- unsafeExt(_ + ".show") (
- opacity(1),
- transition := "opacity 200ms ease-in-out",
- ),
- unsafeExt(_ + ".close") (
- display.none
- )
- )
-
- val mobileBoxActionPanel = style(
- boxShadow := "0px 0px 2px #555",
- backgroundColor(conf.colors.main),
- color(conf.colors.mainText),
- position.fixed,
- left(0 px),
- bottom(0 px),
- height.auto,
- width(100 %%),
- border.`0`,
- fontSize(20 px),
- zIndex(21),
- padding(20 px),
- unsafeChild("button") (
- backgroundColor(conf.colors.main).important,
- color(conf.colors.mainText).important,
- textTransform.uppercase
- )
- )
-
- val mobileMenu = style(
- position.absolute,
- padding(10 px),
- top(49 px),
- left.`0`,
- width(100 %%),
- backgroundColor(conf.colors.main),
- zIndex(10),
- textAlign.right,
- unsafeChild("div") (
- margin.vertical(10 px)
- ),
- unsafeChild("a") (
- fontSize(18 px),
- color(conf.colors.link)
- ),
- unsafeChild("hr") (
- marginTop(2 rem),
- border.`0`
- ),
- boxShadow := "0px 4px 4px #bbb"
- )
-
- val hrThin = style(
- marginTop(15 px),
- marginBottom(10 px),
+ ),
+ unsafeExt(_ + ".close") (
+ display.none
+ )
+ )
+
+ val mobileBoxActionPanel = style(
+ boxShadow := "0px 0px 2px #555",
+ backgroundColor(conf.colors.main),
+ color(conf.colors.mainText),
+ position.fixed,
+ left(0 px),
+ bottom(0 px),
+ height.auto,
+ width(100 %%),
+ border.`0`,
+ fontSize(20 px),
+ zIndex(21),
+ padding(20 px),
+ unsafeChild("button") (
+ backgroundColor(conf.colors.main).important,
+ color(conf.colors.mainText).important,
+ textTransform.uppercase
+ )
+ )
+
+ val mobileMenu = style(
+ position.absolute,
+ padding(10 px),
+ top(49 px),
+ left.`0`,
+ width(100 %%),
+ backgroundColor(conf.colors.main),
+ zIndex(10),
+ textAlign.right,
+ unsafeChild("div") (
+ margin.vertical(10 px)
+ ),
+ unsafeChild("a") (
+ fontSize(18 px),
+ color(conf.colors.link)
+ ),
+ unsafeChild("hr") (
+ marginTop(2 rem),
border.`0`
- )
-
- val labelRequired = style(
- Font.bold
- )
-
- val notNullable = style(
- // borderColor(StyleConstants.Colors.orange),
- // borderStyle.solid,
- // borderWidth(2 px)
- )
-
- val smallLabelRequired = style(
- fontSize(conf.requiredFontSize.px),
- color(conf.colors.danger)
- )
-
- val labelNonRequred = style(
- Font.bold
- )
-
- val editor = style(
- inputDefaultWidth,
- media.maxWidth(600 px)(
- width(100 %%)
- ),
- float.right,
- borderStyle.solid,
- borderWidth(1 px),
- borderRadius.`0`,
- borderColor(Colors.GreySemi),
- marginBottom(10 px)
- )
-
- val controlButtons = style(
- display.flex,
- flexWrap.wrap,
- alignItems.center,
- width(100.%%),
- backgroundColor(Colors.GreyExtra),
- unsafeRoot("svg") (
- marginTop(-2.px)
- ),
- )
-
- val controlButtonsBottom = style(
- borderBottomStyle.solid,
- borderBottomWidth(1 px),
- borderBottomColor(conf.colors.main)
- )
-
- val controlInputs = style(
- display.flex,
- flexWrap.wrap,
- alignItems.center,
- width(100.%%),
- backgroundColor(Colors.GreyExtra),
- unsafeChild("input") (
- margin.horizontal(5 px),
- width.auto,
- flexGrow(1),
- alignItems.center
- ),
- unsafeChild("select") (
- margin.horizontal(5 px),
- flexGrow(1),
- alignItems.center
- ),
- )
-
- val mapPopup = style(
- backgroundColor.white,
- borderStyle.solid,
- borderWidth(1 px),
- borderColor(conf.colors.main),
- padding.vertical(5 px),
- padding.horizontal(10 px),
- marginLeft(5 px),
- marginTop(5 px),
- unsafeChild("ul") (
- paddingLeft.`0`,
- marginBottom.`0`
- ),
- unsafeChild("li") (
- listStyle := "none",
- padding(5 px),
- &.hover(
- color.white,
- backgroundColor(conf.colors.main)
- )
- )
- )
-
- val mapSearch = style(
- display.flex,
- backgroundColor(Colors.GreyExtra),
- unsafeChild("input") (
- width(100.%%),
- margin(5 px),
- backgroundColor.white
- ),
- height(33 px),
- marginBottom(-33 px),
- zIndex(2),
- position.relative
- )
-
- val mapInfo = style(
- color(Colors.Grey),
- padding.horizontal(10 px),
- padding.vertical(5 px),
- backgroundColor(Colors.GreyExtra),
- )
-
- val mapInfoChild = style(
- display.flex,
- unsafeChild("input") (
- width(100.%%),
- margin(5 px),
- backgroundColor.white
- )
- )
-
- val mapGeomAction = style(
- display.flex,
- flexDirection.column
- )
-
- val mapButton = style(
- color(conf.colors.main),
- backgroundColor(Colors.GreyExtra),
- padding(5 px,10 px),
- border.`0`,
- unsafeRoot(".active")(
- backgroundColor(conf.colors.main),
- color(conf.colors.mainText)
- ),
- &.attrExists("disabled") (
- color(gray),
- backgroundColor(Colors.GreyExtra),
- )
- )
-
- val mapLayerSelect = style(
- marginLeft(10 px),
- backgroundColor.transparent,
+ ),
+ boxShadow := "0px 4px 4px #bbb"
+ )
+
+ val hrThin = style(
+ marginTop(15 px),
+ marginBottom(10 px),
+ border.`0`
+ )
+
+ val labelRequired = style(
+ Font.bold
+ )
+
+ val notNullable = style(
+// borderColor(StyleConstants.Colors.orange),
+// borderStyle.solid,
+// borderWidth(2 px)
+ )
+
+ val smallLabelRequired = style(
+ fontSize(conf.requiredFontSize.px),
+ color(conf.colors.danger)
+ )
+
+ val labelNonRequred = style(
+ Font.bold
+ )
+
+ val editor = style(
+ inputDefaultWidth,
+ media.maxWidth(600 px)(
+ width(100 %%)
+ ),
+ float.right,
+ borderStyle.solid,
+ borderWidth(1 px),
+ borderRadius.`0`,
+ borderColor(Colors.GreySemi),
+ marginBottom(10 px)
+ )
+
+ val controlButtons = style(
+ display.flex,
+ flexWrap.wrap,
+ alignItems.center,
+ width(100.%%),
+ backgroundColor(Colors.GreyExtra),
+ unsafeRoot("svg") (
+ marginTop(-2.px)
+ ),
+ )
+
+ val controlButtonsBottom = style(
+ borderBottomStyle.solid,
+ borderBottomWidth(1 px),
+ borderBottomColor(conf.colors.main)
+ )
+
+ val controlInputs = style(
+ display.flex,
+ flexWrap.wrap,
+ alignItems.center,
+ width(100.%%),
+ backgroundColor(Colors.GreyExtra),
+ unsafeChild("input") (
+ margin.horizontal(5 px),
+ width.auto,
+ flexGrow(1),
+ alignItems.center
+ ),
+ unsafeChild("select") (
+ margin.horizontal(5 px),
+ flexGrow(1),
+ alignItems.center
+ ),
+ )
+
+ val mapPopup = style(
+ backgroundColor.white,
+ borderStyle.solid,
+ borderWidth(1 px),
+ borderColor(conf.colors.main),
+ padding.vertical(5 px),
+ padding.horizontal(10 px),
+ marginLeft(5 px),
+ marginTop(5 px),
+ unsafeChild("ul") (
+ paddingLeft.`0`,
+ marginBottom.`0`
+ ),
+ unsafeChild("li") (
+ listStyle := "none",
+ padding(5 px),
&.hover(
- backgroundColor.transparent
- ),
- &.focus(
- backgroundColor.transparent
- ),
- unsafeChild("option") (
- color.black
- )
- )
-
- val mapLayerSelectFullscreen = style(
- position.absolute,
- right.`0`,
- bottom(36 px),
- backgroundColor.rgba(255,255,255,0.8),
- padding.horizontal(10 px),
- padding.vertical(5 px),
- unsafeChild("select") {
- width(90 %%)
- },
- zIndex(1),
- fontSize(11 px),
- lineHeight(22 px),
- media.maxWidth(600 px)(
- bottom(100 px)
+ color.white,
+ backgroundColor(conf.colors.main)
)
)
+ )
- val mapFullscreen = style(
- position.fixed,
- top(50 px),
- left.`0`,
- width(100 %%),
- backgroundColor.white,
- zIndex(10),
- height :=! "calc(100vh - 105px)"
- )
-
- val simpleInputBottomBorder = style(
- margin.`0`,
- padding.`0`,
+ val mapSearch = style(
+ display.flex,
+ backgroundColor(Colors.GreyExtra),
+ unsafeChild("input") (
width(100.%%),
- height(100.%%),
- float.none.important,
- )
-
- val simpleInput = style(
- margin.`0`,
- padding.`0`,
+ margin(5 px),
+ backgroundColor.white
+ ),
+ height(33 px),
+ marginBottom(-33 px),
+ zIndex(2),
+ position.relative
+ )
+
+ val mapInfo = style(
+ color(Colors.Grey),
+ padding.horizontal(10 px),
+ padding.vertical(5 px),
+ backgroundColor(Colors.GreyExtra),
+ )
+
+ val mapInfoChild = style(
+ display.flex,
+ unsafeChild("input") (
width(100.%%),
- height(100.%%),
- float.none.important,
- borderWidth(0 px,0 px,1 px,0 px)
- )
-
- val simpleCheckbox = style(
- width(13 px),
- height(13 px),
- float.none.important,
- borderWidth.`0`,
- border.`0`,
- margin.auto,
- display.flex,
- alignSelf.center
- )
-
- val centredContent = style(
- maxWidth(900.px),
- marginTop(20 px),
- marginLeft.auto,
- marginTop(20 px),
- marginRight.auto,
+ margin(5 px),
+ backgroundColor.white
)
+ )
+ val mapGeomAction = style(
+ display.flex,
+ flexDirection.column
+ )
- val margin0Auto = style(
- margin(`0`,auto)
+ val mapButton = style(
+ color(conf.colors.main),
+ backgroundColor(Colors.GreyExtra),
+ padding(5 px,10 px),
+ border.`0`,
+ unsafeExt(_ + ".active")(
+ backgroundColor(conf.colors.main),
+ color(conf.colors.mainText)
+ ),
+ &.attrExists("disabled") (
+ color(gray),
+ backgroundColor(Colors.GreyExtra),
)
-
-
- val tristateCheckBox = style(
- backgroundColor(c"#fff"),
- borderStyle.solid,
- borderWidth(1 px),
- borderColor(Colors.GreySemi),
- cursor.pointer,
- display.inlineBlock,
+ )
+
+ val mapLayerSelect = style(
+ marginLeft(10 px),
+ backgroundColor.transparent,
+ &.hover(
+ backgroundColor.transparent
+ ),
+ &.focus(
+ backgroundColor.transparent
+ ),
+ unsafeChild("option") (
+ color.black
+ )
+ )
+
+ val mapLayerSelectFullscreen = style(
+ position.absolute,
+ right.`0`,
+ bottom(36 px),
+ backgroundColor.rgba(255,255,255,0.8),
+ padding.horizontal(10 px),
+ padding.vertical(5 px),
+ unsafeChild("select") {
+ width(90 %%)
+ },
+ zIndex(1),
+ fontSize(11 px),
+ lineHeight(22 px),
+ media.maxWidth(600 px)(
+ bottom(100 px)
+ )
+ )
+
+ val mapFullscreen = style(
+ position.fixed,
+ top(50 px),
+ left.`0`,
+ width(100 %%),
+ backgroundColor.white,
+ zIndex(10),
+ height :=! "calc(100vh - 105px)"
+ )
+
+
+ override val mapTable = style(
+ height :=! "calc(100vh - 105px)",
+ media.maxWidth(600 px)(
+ height :=! "calc(100vh - 50px)",
+ ),
+ position.sticky,
+ top.`0`
+ )
+
+
+ val simpleInputBottomBorder = style(
+ margin.`0`,
+ padding.`0`,
+ width(100.%%),
+ height(100.%%),
+ float.none.important,
+ )
+
+ val simpleInput = style(
+ margin.`0`,
+ padding.`0`,
+ width(100.%%),
+ height(100.%%),
+ float.none.important,
+ borderWidth(0 px,0 px,1 px,0 px)
+ )
+
+ val simpleCheckbox = style(
+ width(13 px),
+ height(13 px),
+ float.none.important,
+ borderWidth.`0`,
+ border.`0`,
+ margin.auto,
+ display.flex,
+ alignSelf.center
+ )
+
+ val centredContent = style(
+ maxWidth(900.px),
+ marginTop(20 px),
+ marginLeft.auto,
+ marginTop(20 px),
+ marginRight.auto,
+ )
+
+
+ val margin0Auto = style(
+ margin(`0`,auto)
+ )
+
+
+ val tristateCheckBox = style(
+ backgroundColor(c"#fff"),
+ borderStyle.solid,
+ borderWidth(1 px),
+ borderColor(Colors.GreySemi),
+ cursor.pointer,
+ display.inlineBlock,
+ height(20 px),
+ width(20 px),
+ marginLeft(10 px),
+ textAlign.center,
+ verticalAlign.middle,
+ unsafeChild("svg")(
+ marginTop(-4 px),
+ svgStroke(c"#fff")
+ )
+ )
+
+ val tristatePositive = style(
+ backgroundColor(c"#4aca65"),
+ borderColor(c"#43b45b")
+ )
+
+ val tristateNegative = style(
+ backgroundColor(c"#dc4e4e"),
+ borderColor(c"#c74545")
+ )
+
+ val label50 = style(
+ width((100-conf.inputPercentage) %%),
+ display.inlineBlock
+ )
+
+ val inputRightLabel = style(
+ width((100-conf.inputPercentage) %%),
+ padding.horizontal(10 px),
+ textAlign.right
+ )
+
+ val childAddButton = style(
+ lineHeight(40 px),
+ paddingLeft(10 px),
+ fontSize(14 px),
+ display.inlineBlock,
+ unsafeChild("svg")(
+ color(conf.colors.main),
height(20 px),
width(20 px),
- marginLeft(10 px),
- textAlign.center,
- verticalAlign.middle,
- unsafeChild("svg")(
- marginTop(-4 px),
- svgStroke(c"#fff")
- )
- )
-
- val tristatePositive = style(
- backgroundColor(c"#4aca65"),
- borderColor(c"#43b45b")
- )
-
- val tristateNegative = style(
- backgroundColor(c"#dc4e4e"),
- borderColor(c"#c74545")
- )
-
- val label50 = style(
- width((100-conf.inputPercentage) %%),
- display.inlineBlock
- )
-
- val inputRightLabel = style(
- width((100-conf.inputPercentage) %%),
- padding.horizontal(10 px),
- textAlign.right
- )
-
- val childAddButton = style(
- lineHeight(40 px),
- paddingLeft(10 px),
- fontSize(14 px),
- display.inlineBlock,
- unsafeChild("svg")(
- color(conf.colors.main),
- height(20 px),
- width(20 px),
- marginRight(5 px)
- )
- )
-
- val twoListContainer = style(
- backgroundColor(Colors.GreyExtra),
- padding(10 px),
- margin(5 px),
- overflow.auto,
- maxHeight(500 px)
- )
-
- val twoListLeft = style(
- unsafeChild(".right") (
- display.none
- )
+ marginRight(5 px)
+ )
+ )
+
+ val twoListContainer = style(
+ backgroundColor(Colors.GreyExtra),
+ padding(10 px),
+ margin(5 px),
+ overflow.auto,
+ maxHeight(500 px)
+ )
+
+ val twoListLeft = style(
+ unsafeChild(".right") (
+ display.none
+ )
+ )
+
+ val twoListRight = style(
+ unsafeChild(".left") (
+ display.none
+ )
+ )
+
+ val twoListElement = style(
+ lineHeight(31 px),
+ border.solid,
+ borderColor(conf.colors.main),
+ borderWidth(2 px),
+ borderRadius(5 px),
+ textAlign.center,
+ fontWeight.bold
+ )
+
+ val twoListButton = style(
+ lineHeight(21 px),
+ padding(3 px,10 px, 7 px,15 px),
+ fontSize(14 px),
+ display.inlineBlock,
+ border.`0`,
+ color(conf.colors.main),
+ &.hover(
+ backgroundColor(conf.colors.main),
+ color.white
)
+ )
- val twoListRight = style(
- unsafeChild(".left") (
- display.none
- )
- )
+ val childAddButtonBoxed = style(
+ width(100 %%),
+ border(conf.childProps.borderSize px,solid, conf.childProps.borderColor),
+ backgroundColor(conf.childProps.backgroundColor),
+ marginBottom(20 px)
+ )
- val twoListElement = style(
- lineHeight(31 px),
- border.solid,
- borderColor(conf.colors.main),
- borderWidth(2 px),
- borderRadius(5 px),
- textAlign.center,
- fontWeight.bold
+ val childRemoveButton = style(
+ lineHeight(32 px),
+ fontSize(14 px),
+ unsafeChild("svg")(
+ color(conf.colors.danger),
+ height(20 px),
+ width(20 px),
+ marginRight(5 px)
)
+ )
- val twoListButton = style(
- lineHeight(21 px),
- padding(3 px,10 px, 7 px,15 px),
- fontSize(14 px),
- display.inlineBlock,
- border.`0`,
+ val childMoveButton = style(
+ lineHeight(32 px),
+ fontSize(18 px),
+ unsafeChild("svg")(
color(conf.colors.main),
- &.hover(
- backgroundColor(conf.colors.main),
- color.white
- )
- )
-
- val childAddButtonBoxed = style(
- width(100 %%),
- border(conf.childProps.borderSize px,solid, conf.childProps.borderColor),
- backgroundColor(conf.childProps.backgroundColor),
- marginBottom(20 px)
- )
-
- val childRemoveButton = style(
- lineHeight(32 px),
- fontSize(14 px),
- unsafeChild("svg")(
- color(conf.colors.danger),
- height(20 px),
- width(20 px),
- marginRight(5 px)
- )
- )
-
- val childMoveButton = style(
- lineHeight(32 px),
- fontSize(18 px),
- unsafeChild("svg")(
- color(conf.colors.main),
- height(20 px),
- width(20 px),
- marginRight(5 px)
- )
- )
-
- val childDuplicateButton = style(
- lineHeight(32 px),
- fontSize(14 px),
- unsafeChild("svg")(
- color(conf.colors.main),
- height(20 px),
- width(20 px),
- marginRight(5 px)
- )
+ height(20 px),
+ width(20 px),
+ marginRight(5 px)
)
+ )
- val dropFileZone = style(
- minHeight(50 px),
- width(100 %%),
- borderColor(Colors.Trasparent),
- borderStyle.solid,
+ val childDuplicateButton = style(
+ lineHeight(32 px),
+ fontSize(14 px),
+ unsafeChild("svg")(
+ color(conf.colors.main),
+ height(20 px),
+ width(20 px),
+ marginRight(5 px)
+ )
+ )
+
+ val dropFileZone = style(
+ minHeight(50 px),
+ width(100 %%),
+ borderColor(Colors.Trasparent),
+ borderStyle.solid,
+ borderWidth(1 px),
+ &.hover(
+ borderStyle.dashed,
+ borderColor(Colors.Grey),
borderWidth(1 px),
- &.hover(
- borderStyle.dashed,
- borderColor(Colors.Grey),
- borderWidth(1 px),
- ),
- display.flex,
- flexDirection.column,
- justifyContent.center,
- alignItems.center,
- margin.vertical(10 px),
- unsafeChild("p")(
- color(Colors.Grey),
- fontSize(11 px),
- Font.bold
- )
- )
-
- val dropFileZoneDropping = style(
- borderColor(conf.colors.main),
- backgroundColor.rgba(0,0,0,0.3),
- unsafeChild("p")(
- color(conf.colors.main)
- )
- )
-
- val sidebarButton = style(
- position.fixed,
- top(5 px),
- left(5 px),
- zIndex(101),
- media.maxWidth(600 px)(
- display.none
- )
-
+ ),
+ display.flex,
+ flexDirection.column,
+ justifyContent.center,
+ alignItems.center,
+ margin.vertical(10 px),
+ unsafeChild("p")(
+ color(Colors.Grey),
+ fontSize(11 px),
+ Font.bold
)
+ )
- val editableTableEditButton = style(
- fontSize(12 px),
- marginLeft(2 px),
- unsafeChild("svg") (
- color.gray,
- ),
- &.hover(
- unsafeChild("svg") (
- color(conf.colors.main)
- )
- )
+ val dropFileZoneDropping = style(
+ borderColor(conf.colors.main),
+ backgroundColor.rgba(0,0,0,0.3),
+ unsafeChild("p")(
+ color(conf.colors.main)
)
+ )
- val editableTableMulti = style(
- minHeight(20 px),
- padding.`0`
+ val sidebarButton = style(
+ position.fixed,
+ top(5 px),
+ left(5 px),
+ zIndex(101),
+ media.maxWidth(600 px)(
+ display.none
)
- val xyButtonOnTable = style(
- media.maxWidth(600 px)(
- width(100.px)
- )
- )
+ )
- val queryBuilderContainer = style(
- unsafeChild("input")(
- float.none,
- width(200 px)
- ),
- unsafeChild("select")(
- float.none,
- width(200 px)
- )
-
- )
-
- val adminConditionBlock = style(
- margin(20 px),
- display.flex,
- justifyContent.spaceBetween,
- alignItems.center,
- unsafeChild("select")(
- width(300 px)
- ),
- unsafeChild("input")(
- width(300 px)
- )
- )
-
- val adminCreateForm = style(
- display.flex,
- width(400 px),
- justifyContent.spaceBetween,
- alignItems.baseline,
- unsafeChild("select")(
- width(300 px)
+ val editableTableEditButton = style(
+ fontSize(12 px),
+ marginLeft(2 px),
+ unsafeChild("svg") (
+ color.gray,
+ ),
+ &.hover(
+ unsafeChild("svg") (
+ color(conf.colors.main)
)
)
-
- val thOver = style(
- outlineStyle.dashed,
- outlineWidth(1 px),
- outlineOffset(-1 px),
- outlineColor.lightgray,
- backgroundColor.rgba(0,0,0,0.1)
- )
+ )
+
+ val editableTableMulti = style(
+ minHeight(20 px),
+ padding.`0`
+ )
+
+ val xyButtonOnTable = style(
+ media.maxWidth(600 px)(
+ width(100.px)
+ )
+ )
+
+ val queryBuilderContainer = style(
+ unsafeChild("input")(
+ float.none,
+ width(200 px)
+ ),
+ unsafeChild("select")(
+ float.none,
+ width(200 px)
+ )
+
+ )
+
+ val adminConditionBlock = style(
+ margin(20 px),
+ display.flex,
+ justifyContent.spaceBetween,
+ alignItems.center,
+ unsafeChild("select")(
+ width(300 px)
+ ),
+ unsafeChild("input")(
+ width(300 px)
+ )
+ )
+
+ val adminCreateForm = style(
+ display.flex,
+ width(400 px),
+ justifyContent.spaceBetween,
+ alignItems.baseline,
+ unsafeChild("select")(
+ width(300 px)
+ )
+ )
+
+ val thOver = style(
+ outlineStyle.dashed,
+ outlineWidth(1 px),
+ outlineOffset(-1 px),
+ outlineColor.lightgray,
+ backgroundColor.rgba(0,0,0,0.1)
+ )
//
// val mapPopup = style(
@@ -1671,19 +1710,18 @@ object GlobalStyleFactory{
// )
//
- // val fixedHeader = style(
- // unsafeRoot("tbody")(
- //// display.block,
- // overflow.auto,
- // maxHeight :=! "calc(100vh - 330px)",
- //// height(200 px),
- //// width(100 %%)
- // ),
- // unsafeRoot("thead")(
- //// display.block
- // )
- // )
-
-
- }
-}
\ No newline at end of file
+// val fixedHeader = style(
+// unsafeRoot("tbody")(
+//// display.block,
+// overflow.auto,
+// maxHeight :=! "calc(100vh - 330px)",
+//// height(200 px),
+//// width(100 %%)
+// ),
+// unsafeRoot("thead")(
+//// display.block
+// )
+// )
+
+
+}
diff --git a/client/src/main/scala/ch/wsl/box/client/utils/ImageUtils.scala b/client/src/main/scala/ch/wsl/box/client/utils/ImageUtils.scala
new file mode 100644
index 000000000..c28509257
--- /dev/null
+++ b/client/src/main/scala/ch/wsl/box/client/utils/ImageUtils.scala
@@ -0,0 +1,55 @@
+package ch.wsl.box.client.utils
+
+import ch.wsl.box.client.services.BrowserConsole
+import ch.wsl.box.client.vendors.CompressorJS
+import ch.wsl.typings.compressorjs
+import org.scalajs.dom
+import org.scalajs.dom.{Blob, Image}
+import scribe.Logging
+
+import scala.concurrent.{Future, Promise}
+import scala.scalajs.js
+
+case class ImageDimensions(h:Int,w:Int) {
+ def scaled_w(new_h:Int):Int = (new_h.toDouble / h * w).toInt
+}
+
+object ImageUtils extends Logging {
+ def dimensions(image:String): Future[ImageDimensions] = {
+ val p = Promise[ImageDimensions]()
+ val img = new Image()
+ img.src = image
+ img.onload = _ => {
+ p.success(ImageDimensions(img.naturalHeight,img.naturalWidth))
+ }
+ p.future
+ }
+
+
+ def setHeight(img:Blob,height:Int) = {
+
+ val p = Promise[Blob]()
+ BrowserConsole.log(img)
+ BrowserConsole.log(img.toString)
+ println(ch.wsl.typings.isBlob.mod.default(img))
+
+ dom.window.asInstanceOf[js.Dynamic].tmpBlob = img
+
+ val options = compressorjs.Compressor.Options()
+ .setHeight(height)
+ .setSuccess{ b =>
+ p.success(b.asInstanceOf[Blob])
+ }
+ .setError{ e =>
+ logger.warn(e.message)
+ p.failure(new Exception(e.message))
+ }
+ new CompressorJS(img,options)
+
+
+
+ p.future
+
+ }
+
+}
diff --git a/client/src/main/scala/ch/wsl/box/client/utils/Shorten.scala b/client/src/main/scala/ch/wsl/box/client/utils/Shorten.scala
index 602b9a98e..ab1aeca00 100644
--- a/client/src/main/scala/ch/wsl/box/client/utils/Shorten.scala
+++ b/client/src/main/scala/ch/wsl/box/client/utils/Shorten.scala
@@ -1,8 +1,14 @@
package ch.wsl.box.client.utils
+
+
object Shorten {
+
+
+
+
def apply(str:String,maxLength:Int = 40):String = {
- val noHTML = ch.wsl.typings.striptags.mod.apply(str)
+ val noHTML = StripHtml(str)
if(noHTML.length > maxLength) {
str.take(maxLength-3) + "..."
} else str
diff --git a/client/src/main/scala/ch/wsl/box/client/utils/StripHtml.scala b/client/src/main/scala/ch/wsl/box/client/utils/StripHtml.scala
new file mode 100644
index 000000000..9d985826d
--- /dev/null
+++ b/client/src/main/scala/ch/wsl/box/client/utils/StripHtml.scala
@@ -0,0 +1,21 @@
+package ch.wsl.box.client.utils
+
+
+import scala.scalajs.js
+import scala.scalajs.js.annotation.JSImport
+
+
+
+object StripHtml {
+
+ @js.native
+ @JSImport("string-strip-html","stripHtml")
+ private object StringStripHtml extends js.Object {
+ def apply(input: String): js.Dynamic = js.native
+ }
+
+ def apply(s:String):String = {
+ StringStripHtml(s).result.asInstanceOf[String]
+ }
+
+}
diff --git a/client/src/main/scala/ch/wsl/box/client/vendors/CompressorJS.scala b/client/src/main/scala/ch/wsl/box/client/vendors/CompressorJS.scala
new file mode 100644
index 000000000..6610b0e64
--- /dev/null
+++ b/client/src/main/scala/ch/wsl/box/client/vendors/CompressorJS.scala
@@ -0,0 +1,12 @@
+package ch.wsl.box.client.vendors
+
+import org.scalajs.dom.Blob
+
+import scala.scalajs.js
+import scala.scalajs.js.annotation.JSImport
+
+@JSImport("compressorjs", JSImport.Default)
+@js.native
+class CompressorJS(blob:Blob,options:js.Any) extends js.Object {
+
+}
diff --git a/client/src/main/scala/ch/wsl/box/client/vendors/PgLiteWorker.scala b/client/src/main/scala/ch/wsl/box/client/vendors/PgLiteWorker.scala
new file mode 100644
index 000000000..c193fc6d9
--- /dev/null
+++ b/client/src/main/scala/ch/wsl/box/client/vendors/PgLiteWorker.scala
@@ -0,0 +1,11 @@
+package ch.wsl.box.client.vendors
+
+import ch.wsl.typings.electricSqlPglite.distPgliteBdaQNAoWMod.BasePGlite
+import org.scalajs.dom.{Blob, Worker}
+
+import scala.scalajs.js
+import scala.scalajs.js.annotation.JSImport
+
+@JSImport("@electric-sql/pglite/worker", "PGliteWorker")
+@js.native
+class PGliteWorker(worker:Worker) extends js.Object with BasePGlite
diff --git a/client/src/main/scala/ch/wsl/box/client/views/DataView.scala b/client/src/main/scala/ch/wsl/box/client/views/DataView.scala
index 9f89743f5..ff4b28370 100755
--- a/client/src/main/scala/ch/wsl/box/client/views/DataView.scala
+++ b/client/src/main/scala/ch/wsl/box/client/views/DataView.scala
@@ -152,7 +152,7 @@ case class DataView(model:ModelProperty[DataModel], presenter:DataPresenter) ext
metadata.label
),
produce(model.subProp(_.exportDef)){ ed =>
- div(ClientConf.style.global)(ed.map(_.description.getOrElse[String]("")).getOrElse[String]("")).render
+ div(ed.map(_.description.getOrElse[String]("")).getOrElse[String]("")).render
},
br,
JSONMetadataRenderer(metadata, model.subProp(_.queryData),Seq(),Property(model.get.queryData.ID(metadata.keyFields).map(_.asString)),WidgetCallbackActions.noAction,Property(false),false).edit(nested),
diff --git a/client/src/main/scala/ch/wsl/box/client/views/EntitiesView.scala b/client/src/main/scala/ch/wsl/box/client/views/EntitiesView.scala
index 410be9391..063d922f4 100755
--- a/client/src/main/scala/ch/wsl/box/client/views/EntitiesView.scala
+++ b/client/src/main/scala/ch/wsl/box/client/views/EntitiesView.scala
@@ -7,7 +7,6 @@ package ch.wsl.box.client.views
import ch.wsl.box.client.routes.Routes
import ch.wsl.box.client.services.{BrowserConsole, ClientConf, Labels, Navigate, REST, UI}
import ch.wsl.box.client.styles.BootstrapCol
-import ch.wsl.box.client.styles.GlobalStyleFactory.GlobalStyles
import ch.wsl.box.client.{EntitiesState, EntityFormState, EntityTableState}
import ch.wsl.box.model.shared.EntityKind
import io.udash._
diff --git a/client/src/main/scala/ch/wsl/box/client/views/EntityFormView.scala b/client/src/main/scala/ch/wsl/box/client/views/EntityFormView.scala
index f2005abc6..3ceab97a2 100755
--- a/client/src/main/scala/ch/wsl/box/client/views/EntityFormView.scala
+++ b/client/src/main/scala/ch/wsl/box/client/views/EntityFormView.scala
@@ -304,8 +304,24 @@ case class EntityFormPresenter(model:ModelProperty[EntityFormModel]) extends Pre
val errors = document.querySelectorAll("*:invalid")
for(i <- 0 to errors.length) {
errors.item(i) match {
- case e:HTMLElement => logger.warn(s"Error on: ${e.outerHTML}")
- case _ => logger.warn(s"Error on non HTMLElement")
+ case e:dom.HTMLElement => {
+ logger.warn(s"Error on: ${e} cass: ${e.className}")
+ window.asInstanceOf[js.Dynamic].testEL = e
+ e.closest(".box-tab") match {
+ case element: dom.HTMLElement => element.dataset.get("boxTab") match {
+ case Some(value) => document.getElementById(JSONMetadataRenderer.tabSelectorId(value)) match {
+ case element: dom.HTMLElement => {
+ element.click()
+ _form.reportValidity()
+ }
+ case _ => ???
+ }
+ case None => logger.warn("Box tab element not found")
+ }
+ case _ => logger.warn("Non HTMLElement box-tab")
+ }
+ }
+ case e => logger.warn(s"Error on non HTMLElement: ${e}")
}
}
logger.warn(s"Form validation failed")
diff --git a/client/src/main/scala/ch/wsl/box/client/views/EntityTableView.scala b/client/src/main/scala/ch/wsl/box/client/views/EntityTableView.scala
index e9f32ea7d..fe84a4242 100755
--- a/client/src/main/scala/ch/wsl/box/client/views/EntityTableView.scala
+++ b/client/src/main/scala/ch/wsl/box/client/views/EntityTableView.scala
@@ -4,7 +4,7 @@ import ch.wsl.box.client.Context.services
import ch.wsl.box.client.db.{DB, LocalRecord}
import ch.wsl.box.client.routes.Routes
import ch.wsl.box.client.{Context, EntityFormState, EntityTableState, FormPageState}
-import ch.wsl.box.client.services.{BrowserConsole, ClientConf, Labels, Navigate, Navigation, Notification, UI}
+import ch.wsl.box.client.services.{BrowserConsole, ClientConf, Labels, Navigate, Navigation, Notification, PDF, UI}
import ch.wsl.box.client.styles.Icons.Icon
import ch.wsl.box.client.styles.{BootstrapCol, Icons}
import ch.wsl.box.client.utils.{ElementId, TestHooks, URLQuery}
@@ -160,7 +160,7 @@ case class EntityTablePresenter(model:ModelProperty[EntityTableModel], onSelect:
}
}
- private def _handleState(state: EntityTableState,emptyFieldsForm:JSONMetadata): Unit = {
+ private def _handleState(state: EntityTableState,metadata:JSONMetadata): Unit = {
services.clientSession.loading.set(true)
logger.info(s"handling Entity table state name=${state.entity}, kind=${state.kind} and query=${state.query}")
@@ -169,9 +169,6 @@ case class EntityTablePresenter(model:ModelProperty[EntityTableModel], onSelect:
val urlQuery:Option[JSONQuery] = URLQuery.fromQueryParameters(Routes.urlParams.get("q")).orElse(stateQuery)
services.clientSession.setURLQuery(urlQuery.getOrElse(JSONQuery.empty))
- val fields = emptyFieldsForm.fields.filter(field => emptyFieldsForm.tabularFields.contains(field.name))
- val form = emptyFieldsForm.copy(fields = fields)
-
val defaultQuery:JSONQuery = JSONQuery.empty.limit(ClientConf.pageLength)
val queryWithGeom:JSONQuery = services.clientSession.getQueryFor(state.kind,state.entity,urlQuery) match {
@@ -179,12 +176,12 @@ case class EntityTablePresenter(model:ModelProperty[EntityTableModel], onSelect:
case _ => urlQuery.getOrElse(defaultQuery)
}
- val geomFields = emptyFieldsForm.geomFields.map(_.name)
+ val geomFields = metadata.geomFields.map(_.name)
val query = queryWithGeom.copy(filter = queryWithGeom.filter.filterNot(f => geomFields.contains(f.column)))
{for{
access <- { if(!state.public)
- services.rest.tableAccess(form.entity,state.kind)
+ services.rest.tableAccess(metadata.entity,state.kind)
else Future.successful(TableAccess(false,false,false))
}
specificKind <- { if(!state.public)
@@ -199,7 +196,7 @@ case class EntityTablePresenter(model:ModelProperty[EntityTableModel], onSelect:
kind = specificKind,
urlQuery = urlQuery,
rows = Seq(),
- fieldQueries = form.table.map{ field =>
+ fieldQueries = metadata.table.map{ field =>
val operator = query.filter.find(_.column == field.name).flatMap(_.operator).getOrElse(Filter.default(field))
val rawValue = query.filter.find(_.column == field.name).flatMap(_.value).getOrElse("")
@@ -211,7 +208,7 @@ case class EntityTablePresenter(model:ModelProperty[EntityTableModel], onSelect:
filterOperator = operator
)
},
- metadata = Some(form),
+ metadata = Some(metadata),
selectedRow = Seq(),
ids = IDsVMFactory.empty,
pages = Navigation.pageCount(0),
@@ -221,7 +218,7 @@ case class EntityTablePresenter(model:ModelProperty[EntityTableModel], onSelect:
geoms = Seq(),
extent = None,
public = state.public,
- selectedColumns = form.table
+ selectedColumns = metadata.preselectedTable
)
//saveIds(IDs(true,1,Seq(),0),query)
@@ -551,6 +548,11 @@ case class EntityTablePresenter(model:ModelProperty[EntityTableModel], onSelect:
e.preventDefault()
}
+ val downloadPdf = (e: Event) => {
+ download("pdf")
+ e.preventDefault()
+ }
+
val downloadXLS = (e:Event) => {
download("xlsx")
e.preventDefault()
@@ -583,18 +585,21 @@ case class EntityTablePresenter(model:ModelProperty[EntityTableModel], onSelect:
val kind = EntityKind(model.subProp(_.kind).get).entityOrForm
val modelName = model.subProp(_.name).get
- val exportFields = model.get.metadata.map(_.exportFields).getOrElse(Seq())
- val fields = model.get.metadata.map(_.fields).getOrElse(Seq())
+ val fields = model.get.selectedColumns.map(_.name)
- val queryNoLimits = query(None).copy(paging = None)
+ val queryNoLimits = query(None).copy(paging = None)
- val url = Routes.apiV1(
- s"/$kind/${services.clientSession.lang()}/$modelName/$format?fk=${ExportMode.RESOLVE_FK}&fields=${exportFields.mkString(",")}&q=${URIUtils.encodeURI(queryNoLimits.asJson.noSpaces)}".replaceAll("\n","")
- )
- logger.info(s"downloading: $url")
- dom.window.open(url)
+ if(format == "pdf") {
+ PDF.table(kind,modelName,fields,queryNoLimits)
+ } else {
+ val url = Routes.apiV1(
+ s"/$kind/${services.clientSession.lang()}/$modelName/$format?fk=${ExportMode.RESOLVE_FK}&fields=${fields.mkString(",")}&q=${URIUtils.encodeURI(queryNoLimits.asJson.noSpaces)}".replaceAll("\n", "")
+ )
+ logger.info(s"downloading: $url")
+ dom.window.open(url)
+ }
}
def getObj(id:JSONID):Future[Json] = {
@@ -774,11 +779,7 @@ case class EntityTableView(model:ModelProperty[EntityTableModel], presenter:Enti
if(model.subProp(_.geoms).get.isEmpty)
presenter.loadGeoms(model.subProp(_.extent).get)
- if(window.innerWidth < 600) { // is mobile
- map = Some(div(height := (window.innerHeight - 50).px).render)
- } else {
- map = Some(div(height := (window.innerHeight - 105).px).render)
- }
+ map = Some(div(ClientConf.style.mapTable).render)
val observer = new MutationObserver({ (mutations, observer) =>
@@ -1026,7 +1027,7 @@ case class EntityTableView(model:ModelProperty[EntityTableModel], presenter:Enti
} else {
el.querySelector(ClientConf.style.tableHeader.selector)
}
- head.innerText
+ head.innerHTML
}
new TableColumnDrag(table, labelExtractor,e => {
@@ -1058,7 +1059,7 @@ case class EntityTableView(model:ModelProperty[EntityTableModel], presenter:Enti
headerFactory = Some(_ => div(Labels.table.column_selection).render),
bodyFactory = Some { nested =>
div(
- metadata.toSeq.flatMap(_.table).filterNot(_.`type` == JSONFieldTypes.GEOMETRY).map{ c =>
+ metadata.toSeq.flatMap(_.fields).filterNot(_.`type` == JSONFieldTypes.GEOMETRY).map{ c =>
div(
Checkbox(localModel.bitransform(_.contains(c)){
case true => localModel.get ++ Seq(c)
@@ -1144,9 +1145,13 @@ case class EntityTableView(model:ModelProperty[EntityTableModel], presenter:Enti
),
div(id := "box-table", ClientConf.style.fullHeightMax,ClientConf.style.tableHeaderFixed,
- tableContent(metadata),
+ div(
+ ClientConf.style.fullHeightMax,
+ tableContent(metadata)
+ ),
button(`type` := "button", onclick :+= presenter.downloadCSV, ClientConf.style.boxButton, Labels.entity.csv),
button(`type` := "button", onclick :+= presenter.downloadXLS, ClientConf.style.boxButton, Labels.entity.xls),
+ button(`type` := "button", onclick :+= presenter.downloadPdf, ClientConf.style.boxButton, Labels.entity.pdf),
button(`type` := "button", onclick :+= presenter.importXLS, ClientConf.style.boxButton, Labels.entity.importxls),
if (presenter.hasGeometry()) {
Seq(
diff --git a/client/src/main/scala/ch/wsl/box/client/views/LoginView.scala b/client/src/main/scala/ch/wsl/box/client/views/LoginView.scala
index 972901e63..984c53974 100755
--- a/client/src/main/scala/ch/wsl/box/client/views/LoginView.scala
+++ b/client/src/main/scala/ch/wsl/box/client/views/LoginView.scala
@@ -29,7 +29,8 @@ case class LoginPresenter() extends Presenter[LoginStateAbstract] {
override def handleState(state: LoginStateAbstract): Unit = {}
def login(model:ModelProperty[LoginData]) = {
- services.clientSession.login(model.get.username,model.get.password).recover { case _ =>
+ services.clientSession.login(model.get.username,model.get.password).recover { case t =>
+ t.printStackTrace()
model.subProp(_.message).set(Labels.login.failed)
}
}
@@ -44,7 +45,7 @@ case class LoginView(presenter:LoginPresenter) extends View {
override def getTemplate = div(
div(BootstrapStyles.container)(raw(UI.loginTopHtml)),
- div(BootstrapStyles.container, height := 400.px)(
+ div(BootstrapStyles.container, `class` := "login-container", height := 400.px)(
div(BootstrapStyles.Grid.row,
BootstrapStyles.Flex.justifyContent(BootstrapStyles.FlexContentJustification.Center),
BootstrapStyles.Flex.alignItems(BootstrapStyles.FlexAlign.Center),
diff --git a/client/src/main/scala/ch/wsl/box/client/views/RootView.scala b/client/src/main/scala/ch/wsl/box/client/views/RootView.scala
index 37383fa8c..fc5498e50 100755
--- a/client/src/main/scala/ch/wsl/box/client/views/RootView.scala
+++ b/client/src/main/scala/ch/wsl/box/client/views/RootView.scala
@@ -119,21 +119,8 @@ class RootView(viewModel:ModelProperty[RootViewModel]) extends ContainerView {
private def content = produce(viewModel.subProp(_.layout)) {
- case Layouts.std => {
- div(BootstrapStyles.containerFluid)(
- Header.navbar(UI.title),
- notifications,
- main(ClientConf.style.fullHeight)(
- div()(
- child
- )
- ),
- Footer.template(UI.logo),
- LoginPopup.render
- ).render
- }
+ case Layouts.std => services.layout.container(child)
case Layouts.blank => div(BootstrapStyles.containerFluid,overflowX.hidden,ClientConf.style.showFooterActionOnMobile)(
- notifications,
child
).render
}
@@ -142,6 +129,7 @@ class RootView(viewModel:ModelProperty[RootViewModel]) extends ContainerView {
override def getTemplate: Modifier = div(
loading,
+ notifications,
content
)
diff --git a/client/src/main/scala/ch/wsl/box/client/views/admin/DBRepl.scala b/client/src/main/scala/ch/wsl/box/client/views/admin/DBRepl.scala
index 52d5b6f20..977e4d17c 100644
--- a/client/src/main/scala/ch/wsl/box/client/views/admin/DBRepl.scala
+++ b/client/src/main/scala/ch/wsl/box/client/views/admin/DBRepl.scala
@@ -34,7 +34,7 @@ class DBReplView( presenter:DBReplPresenter) extends View {
val content:HTMLDivElement = div(
- script(src := Routes.baseUri + "assets/@electric-sql/pglite-repl/dist-webcomponent/Repl.js", `type` := "module",onload :+= loaded),
+ script(src := Routes.baseUri + "public/@electric-sql/pglite-repl/dist-webcomponent/Repl.js", `type` := "module",onload :+= loaded),
).render
def loaded(e:Event) = {
diff --git a/client/src/main/scala/ch/wsl/box/client/views/components/BoxMainLayout.scala b/client/src/main/scala/ch/wsl/box/client/views/components/BoxMainLayout.scala
new file mode 100644
index 000000000..ca272420b
--- /dev/null
+++ b/client/src/main/scala/ch/wsl/box/client/views/components/BoxMainLayout.scala
@@ -0,0 +1,29 @@
+package ch.wsl.box.client.views.components
+
+import ch.wsl.box.client.services.{ClientConf, LoginPopup, UI}
+import io.udash.bootstrap.BootstrapStyles
+import scalatags.JsDom.all._
+import scalatags.JsDom.tags2.main
+import ch.wsl.box.client.views.components._
+import ch.wsl.typings.std
+import io.udash.core.Presenter
+import org.scalajs.dom.Element
+import scalacss.ScalatagsCss._
+import scalatags.JsDom.all._
+import io.udash.css.CssView._
+
+class BoxMainLayout extends MainLayout {
+
+ override def container(content: Element): Element = {
+ div(BootstrapStyles.containerFluid)(
+ Header.navbar(UI.title),
+ main(ClientConf.style.fullHeight)(
+ div()(
+ content
+ )
+ ),
+ Footer.template(UI.logo),
+ LoginPopup.render
+ ).render
+ }
+}
diff --git a/client/src/main/scala/ch/wsl/box/client/views/components/Footer.scala b/client/src/main/scala/ch/wsl/box/client/views/components/Footer.scala
index 10aac4838..ef434fe49 100755
--- a/client/src/main/scala/ch/wsl/box/client/views/components/Footer.scala
+++ b/client/src/main/scala/ch/wsl/box/client/views/components/Footer.scala
@@ -11,6 +11,6 @@ object Footer {
def template(logo:Option[String]) = footer(ClientConf.style.noMobile,
div(BootstrapStyles.Float.left())(copyright),
- div(BootstrapStyles.Float.right())( logo.map(x => img(ClientConf.style.headerLogo,src := x)))
+ div(BootstrapStyles.Float.right())( logo.map(x => img(ClientConf.style.headerLogo,src := ClientConf.frontendUrl + x)))
).render
}
\ No newline at end of file
diff --git a/client/src/main/scala/ch/wsl/box/client/views/components/JSONMetadataRenderer.scala b/client/src/main/scala/ch/wsl/box/client/views/components/JSONMetadataRenderer.scala
index 2936112f6..c62310199 100755
--- a/client/src/main/scala/ch/wsl/box/client/views/components/JSONMetadataRenderer.scala
+++ b/client/src/main/scala/ch/wsl/box/client/views/components/JSONMetadataRenderer.scala
@@ -31,8 +31,11 @@ import org.scalajs.dom.{Event, window}
* Created by andre on 4/25/2017.
*/
+object JSONMetadataRenderer {
+ def tabSelectorId(value:String):String = s"box-tab-select-${value.replaceAll(" ","-")}"
+}
-case class JSONMetadataRenderer(metadata: JSONMetadata, data: Property[Json], children: Seq[JSONMetadata], id: ReadableProperty[Option[String]],actions: WidgetCallbackActions,changed:Property[Boolean], public:Boolean) extends ChildWidget {
+case class JSONMetadataRenderer(metadata: JSONMetadata, data: Property[Json], children: Seq[JSONMetadata], _id: ReadableProperty[Option[String]],actions: WidgetCallbackActions,changed:Property[Boolean], public:Boolean) extends ChildWidget {
import ch.wsl.box.client.Context._
@@ -120,7 +123,7 @@ case class JSONMetadataRenderer(metadata: JSONMetadata, data: Property[Json], ch
val blocks: Seq[FormBlock] = metadata.layout.blocks.map { block =>
FormBlock(
- new BlockRendererWidget(WidgetParams(id,data,field,metadata,data,children,actions,public),block.fields,block.layoutType),
+ new BlockRendererWidget(WidgetParams(_id,data,field,metadata,data,children,actions,public),block.fields,block.layoutType),
block.extractFields(metadata),
Some(block),
)
@@ -180,28 +183,46 @@ case class JSONMetadataRenderer(metadata: JSONMetadata, data: Property[Json], ch
val tabKey = SelectedTabKey(metadata.objId,tabGroup)
val selectedTab = Property(services.clientSession.selectedTab(tabKey).orElse(tabs.headOption.flatten))
div(BootstrapCol.md(blks.map(_.block.width).max),
- ul(BootstrapStyles.Navigation.nav, BootstrapStyles.Navigation.tabs, BootstrapStyles.Navigation.fill,
+ ul(BootstrapStyles.Navigation.nav, BootstrapStyles.Navigation.tabs, BootstrapStyles.Navigation.fill,role := "tablist",
tabs.map { tabId =>
val title:String = blocks.find(_.layoutBlock.exists(_.tab == tabId))
.flatMap(_.layoutBlock.flatMap(_.title).flatMap(Internationalization.either(services.clientSession.lang()))).orElse(tabId).getOrElse("")
+ val isSelected = selectedTab.transform(_ == tabId)
+
li(BootstrapStyles.Navigation.item,
- a(BootstrapStyles.Navigation.link,
- BootstrapStyles.active.styleIf(selectedTab.transform(_ == tabId)),
+ //input(`class` := s"box-tab-validator-$tabId",width := 1.px, height := 1.px, padding := 0, border := 0, float.left),
+ button(BootstrapStyles.Navigation.link,
+ id := JSONMetadataRenderer.tabSelectorId(tabId.getOrElse("")),
+ role := "tab",
+ aria.selected.bind(isSelected.transform(_.toString)),
+ aria.controls := s"box-panel-$tabId",
+ tabindex.bind(isSelected.transform{
+ case true => "0"
+ case false => "-1"
+ }),
+ BootstrapStyles.active.styleIf(isSelected),
onclick :+= { (e: Event) =>
selectedTab.set(tabId);
tabId.foreach(n => services.clientSession.setSelectedTab(tabKey,n))
e.preventDefault() },
- title
+ Labels(title)
).render
).render
}
),
- nested(produce(selectedTab) { tabName =>
+ tabs.map{ tabName =>
val block = blks.filter(_.block.tab == tabName)
window.setTimeout(() => block.map(x => x.widget.afterRender()),0)
- renderBlocks(block).render
- })
+ // make tab fill the space
+ val fullwidth = block.map(b => b.copy(b.block.copy(width = 12/block.length)))
+ val isSelected = selectedTab.transform(_ == tabName)
+ isSelected.listen({
+ case true => window.setTimeout(() => block.map(x => x.widget.afterRender()),0)
+ case _ => ()
+ },true)
+ div(id := s"box-panel-${tabName.getOrElse("")}",`class` := "box-tab",attr("data-box-tab") := tabName.getOrElse("noname"), role := "tabpanel", (style := "display: none;").attrIfNot(isSelected), renderBlocks(fullwidth)).render
+ }
)
}
diff --git a/client/src/main/scala/ch/wsl/box/client/views/components/LoginForm.scala b/client/src/main/scala/ch/wsl/box/client/views/components/LoginForm.scala
index d5775399e..a9b292ec9 100644
--- a/client/src/main/scala/ch/wsl/box/client/views/components/LoginForm.scala
+++ b/client/src/main/scala/ch/wsl/box/client/views/components/LoginForm.scala
@@ -48,7 +48,7 @@ case class LoginForm(login: ModelProperty[LoginData] => Unit) {
img(src := openid.logo, maxWidth := 80.px),
onclick :+= ((e:Event) => {
e.preventDefault()
- val redirectUri = URLEncoder.encode(s"${ClientConf.frontendUrl}/authenticate/${openid.provider_id}","UTF-8")
+ val redirectUri = URLEncoder.encode(s"${ClientConf.frontendUrl}authenticate/${openid.provider_id}","UTF-8")
window.location.href = s"${openid.authorize_url}?client_id=${openid.client_id}&scope=${openid.scope}&response_type=code&state=${UUID.randomUUID()}&redirect_uri=$redirectUri"
})
)
diff --git a/client/src/main/scala/ch/wsl/box/client/views/components/MainLayout.scala b/client/src/main/scala/ch/wsl/box/client/views/components/MainLayout.scala
new file mode 100644
index 000000000..9fc27e7f0
--- /dev/null
+++ b/client/src/main/scala/ch/wsl/box/client/views/components/MainLayout.scala
@@ -0,0 +1,8 @@
+package ch.wsl.box.client.views.components
+
+import org.scalajs.dom.Element
+
+
+trait MainLayout {
+ def container(content:Element):Element
+}
diff --git a/client/src/main/scala/ch/wsl/box/client/views/components/MapList.scala b/client/src/main/scala/ch/wsl/box/client/views/components/MapList.scala
index 843a87310..c376fa8c3 100644
--- a/client/src/main/scala/ch/wsl/box/client/views/components/MapList.scala
+++ b/client/src/main/scala/ch/wsl/box/client/views/components/MapList.scala
@@ -22,7 +22,6 @@ import ch.wsl.typings.ol.mod.{MapBrowserEvent, Overlay}
import ch.wsl.typings.ol.objectMod.ObjectEvent
import ch.wsl.typings.ol.viewMod.FitOptions
import ch.wsl.typings.ol.{extentMod, featureMod, formatGeoJSONMod, geomGeometryMod, layerBaseVectorMod, layerMod, mapBrowserEventMod, mod, olStrings, renderFeatureMod, sourceMod, sourceVectorMod, viewMod}
-import ch.wsl.typings.std.PositionOptions
import scala.concurrent.duration.DurationInt
import scala.scalajs.js
diff --git a/client/src/main/scala/ch/wsl/box/client/views/components/TableFieldsRenderer.scala b/client/src/main/scala/ch/wsl/box/client/views/components/TableFieldsRenderer.scala
index 8693f554e..0f76a1b18 100755
--- a/client/src/main/scala/ch/wsl/box/client/views/components/TableFieldsRenderer.scala
+++ b/client/src/main/scala/ch/wsl/box/client/views/components/TableFieldsRenderer.scala
@@ -2,6 +2,7 @@ package ch.wsl.box.client.views.components
import ch.wsl.box.client.routes.Routes
import ch.wsl.box.client.services.{ClientConf, Labels}
+import ch.wsl.box.client.utils.StripHtml
import ch.wsl.box.client.{EntityFormState, EntityTableState}
import ch.wsl.box.model.shared.GeoJson.Geometry
import ch.wsl.box.model.shared.{JSONField, JSONFieldTypes, JSONID, JSONLookup, JSONLookups, WidgetsNames}
@@ -33,7 +34,7 @@ object TableFieldsRenderer extends Logging{
def renderLongText(string: String):Modifier = {
val length = ClientConf.tableMaxTextLength
- val noHTML = ch.wsl.typings.striptags.mod.apply(string)
+ val noHTML = StripHtml(string)
if(noHTML.length <= length) {
p(noHTML)
} else {
diff --git a/client/src/main/scala/ch/wsl/box/client/views/components/ui/Autocomplete.scala b/client/src/main/scala/ch/wsl/box/client/views/components/ui/Autocomplete.scala
index b87049709..8c9d49664 100644
--- a/client/src/main/scala/ch/wsl/box/client/views/components/ui/Autocomplete.scala
+++ b/client/src/main/scala/ch/wsl/box/client/views/components/ui/Autocomplete.scala
@@ -12,16 +12,21 @@ import org.scalajs.dom.html.Div
import org.scalajs.dom.{Event, HTMLDivElement, HTMLInputElement, MutationObserver, MutationObserverInit, document}
import scalatags.JsDom.all._
import scribe.Logging
-import ch.wsl.typings.autocompleter
-import ch.wsl.typings.autocompleter.{autocompleterBooleans, mod}
import scala.concurrent.{ExecutionContext, Future}
import scala.scalajs.js
-import scala.scalajs.js.annotation.JSName
+import scala.scalajs.js.annotation.{JSImport, JSName}
import scala.scalajs.js.{JSON, UndefOr, |}
object Autocomplete extends Logging {
+
+ @js.native
+ @JSImport("autocompleter", JSImport.Default)
+ private object autocomplete extends js.Object {
+ def apply(opt:js.Object):Unit = js.native
+ }
+
def apply[T](prop: Property[Option[T]], fetch: js.Function1[String,Future[Seq[T]]], toLabel: js.Function1[Option[T],String], toSuggestion: js.Function1[T,HTMLDivElement])(modifier: Modifier*)(implicit ec: ExecutionContext, enc:Encoder[T], dec:Decoder[T]): HTMLInputElement = {
val el: HTMLInputElement = input(modifier).render
@@ -41,7 +46,7 @@ object Autocomplete extends Logging {
val preventSubmit = true
@JSName("fetch")
- def fetch_false(text: String, update: js.Function1[js.Array[String] | autocompleterBooleans.`false`, Unit], trigger: mod.EventTrigger, cursorPos: Double): Unit = {
+ def fetch_false(text: String, update: js.Function1[js.Array[String], Unit], trigger: js.Any, cursorPos: Double): Unit = {
logger.info(s"Fetching $text")
fetch(text).map { data =>
val dataJS: js.Array[String] = data.map(x => x.asJson.noSpaces).toJSArray
@@ -78,7 +83,7 @@ object Autocomplete extends Logging {
}
}
}
- autocompleter.mod.^.asInstanceOf[js.Dynamic](options)
+ autocomplete(options)
}
})
diff --git a/client/src/main/scala/ch/wsl/box/client/views/components/ui/TwoPanelResize.scala b/client/src/main/scala/ch/wsl/box/client/views/components/ui/TwoPanelResize.scala
index 201fdbc31..5af2aed91 100644
--- a/client/src/main/scala/ch/wsl/box/client/views/components/ui/TwoPanelResize.scala
+++ b/client/src/main/scala/ch/wsl/box/client/views/components/ui/TwoPanelResize.scala
@@ -16,7 +16,7 @@ import scala.util.Random
// Ref https://phuoc.ng/collection/html-dom/create-resizable-split-views/
class TwoPanelResize(defaultClose:Boolean) {
- val leftDefaultWidth = 40
+ val leftDefaultWidth = 500
case class Style(conf:StyleConf) extends StyleSheet.Inline{
import dsl._
@@ -29,7 +29,7 @@ class TwoPanelResize(defaultClose:Boolean) {
)
val containerLeft = style(
- width(leftDefaultWidth %%),
+ width(leftDefaultWidth px),
if(defaultClose) width.`0` else { media.maxWidth(600 px)(
width.`0` //:= "calc(100% - 15px)"
)},
@@ -129,7 +129,7 @@ class TwoPanelResize(defaultClose:Boolean) {
leftSide.style.width = "100%"
rightSide.style.display = "none"
} else {
- leftSide.style.width = leftDefaultWidth + "%"
+ leftSide.style.width = leftDefaultWidth + "px"
}
document.getElementById(openId).classList.add(style.hide.htmlClass)
diff --git a/client/src/main/scala/ch/wsl/box/client/views/components/widget/PopupWidget.scala b/client/src/main/scala/ch/wsl/box/client/views/components/widget/PopupWidget.scala
index a9522be14..71afa0f10 100644
--- a/client/src/main/scala/ch/wsl/box/client/views/components/widget/PopupWidget.scala
+++ b/client/src/main/scala/ch/wsl/box/client/views/components/widget/PopupWidget.scala
@@ -159,11 +159,10 @@ object PopupWidget extends ComponentWidgetFactory {
private def _render(write:Boolean,nested:Binding.NestedInterceptor) = popup(nested,write,(modal,modalStatus) => {
val tooltip = WidgetUtils.addTooltip(field.tooltip) _
- div(BootstrapCol.md(12),ClientConf.style.noPadding, ClientConf.style.smallBottomMargin,
- BootstrapStyles.Display.flex(),BootstrapStyles.Flex.justifyContent(BootstrapStyles.FlexContentJustification.Between))(
+ div(BootstrapCol.md(12),ClientConf.style.noPadding, ClientConf.style.smallBottomMargin)(
WidgetUtils.toLabel(field,WidgetUtils.LabelRight),
TextInput(params.prop.bitransform(_.string)(x => params.prop.get))(width := 1.px, height := 1.px, padding := 0, border := 0, float.left,WidgetUtils.toNullable(field.nullable)), //in order to use HTML5 validation we insert an hidden field
- tooltip(button(ClientConf.style.popupButton, onclick :+= ((e:Event) => {
+ tooltip(button(ClientConf.style.popupButton,BootstrapStyles.Float.right(), onclick :+= ((e:Event) => {
modalStatus.set(Status.Open)
e.preventDefault()
}),produceLabel).render)._1,
diff --git a/client/src/main/scala/ch/wsl/box/client/views/components/widget/admin/LayoutWidget.scala b/client/src/main/scala/ch/wsl/box/client/views/components/widget/admin/LayoutWidget.scala
index 45ecac7f9..0042f392f 100644
--- a/client/src/main/scala/ch/wsl/box/client/views/components/widget/admin/LayoutWidget.scala
+++ b/client/src/main/scala/ch/wsl/box/client/views/components/widget/admin/LayoutWidget.scala
@@ -2,7 +2,6 @@ package ch.wsl.box.client.views.components.widget.admin
import ch.wsl.box.client.services.{BrowserConsole, ClientConf}
import ch.wsl.box.client.styles.BootstrapCol
-import ch.wsl.box.client.styles.GlobalStyleFactory.GlobalStyles
import ch.wsl.box.client.styles.constants.StyleConstants.Colors
import ch.wsl.box.client.views.components.widget.{ComponentWidgetFactory, Widget, WidgetParams}
import ch.wsl.box.model.shared.{DistributedLayout, JSONField, JSONFieldTypes, JSONQuery, Layout, LayoutBlock, StackedLayout, WidgetsNames}
@@ -20,12 +19,11 @@ import io.udash.bootstrap.BootstrapStyles
import io.udash.bootstrap.tooltip.UdashTooltip
import scalacss.ScalatagsCss._
import io.udash.css._
-import org.scalajs.dom.{DOMParser, Event, HTMLDivElement, HTMLElement, MIMEType, MutationObserver, MutationObserverInit, document}
+import org.scalajs.dom.{DOMParser, Event, HTMLDivElement, HTMLElement, HTMLInputElement, HTMLSelectElement, MIMEType, MutationObserver, MutationObserverInit, document}
import scribe.Logging
import ch.wsl.typings.gridstack.mod._
import ch.wsl.typings.gridstack.distTypesMod._
import ch.wsl.typings.gridstack.gridstackStrings
-import ch.wsl.typings.std.global.{HTMLInputElement, HTMLSelectElement}
import io.udash.bootstrap.utils.BootstrapStyles.Form
import io.udash.bootstrap.utils.BootstrapTags
import org.scalajs.dom.html.Input
diff --git a/client/src/main/scala/ch/wsl/box/client/views/components/widget/array/TwoListWidget.scala b/client/src/main/scala/ch/wsl/box/client/views/components/widget/array/TwoListWidget.scala
index 63738c35d..6cd3c1d5e 100644
--- a/client/src/main/scala/ch/wsl/box/client/views/components/widget/array/TwoListWidget.scala
+++ b/client/src/main/scala/ch/wsl/box/client/views/components/widget/array/TwoListWidget.scala
@@ -2,7 +2,6 @@ package ch.wsl.box.client.views.components.widget.array
import ch.wsl.box.client.services.{BrowserConsole, ClientConf}
import ch.wsl.box.client.styles.{BootstrapCol, Icons}
-import ch.wsl.box.client.styles.GlobalStyleFactory.GlobalStyles
import ch.wsl.box.client.styles.constants.StyleConstants.Colors
import ch.wsl.box.client.views.components.widget.lookup.LookupWidget
import ch.wsl.box.client.views.components.widget.{ComponentWidgetFactory, Widget, WidgetParams}
@@ -27,7 +26,6 @@ import scribe.Logging
import ch.wsl.typings.gridstack.mod._
import ch.wsl.typings.gridstack.distTypesMod._
import ch.wsl.typings.gridstack.gridstackStrings
-import ch.wsl.typings.std.global.{HTMLInputElement, HTMLSelectElement}
import io.udash.bootstrap.utils.BootstrapStyles.Form
import io.udash.bootstrap.utils.{BootstrapTags, UdashIcons}
import org.scalajs.dom.html.Input
diff --git a/client/src/main/scala/ch/wsl/box/client/views/components/widget/geo/OlMapWidget.scala b/client/src/main/scala/ch/wsl/box/client/views/components/widget/geo/OlMapWidget.scala
index 7f3247855..fd6e998d1 100644
--- a/client/src/main/scala/ch/wsl/box/client/views/components/widget/geo/OlMapWidget.scala
+++ b/client/src/main/scala/ch/wsl/box/client/views/components/widget/geo/OlMapWidget.scala
@@ -332,7 +332,7 @@ class OlMapWidget(val id: ReadableProperty[Option[String]], val field: JSONField
def requiredCheckField = TextInput(
_data.bitransform(_.map(_.toString(1).take(5)).getOrElse("")) // check on the actual data is is non empty
(x => _data.get) // the text input will never been changed
- )(width := 1.px, height := 1.px, padding := 0, border := 0, float.left,WidgetUtils.toNullable(field.nullable)) //in order to use HTML5 validation we insert an hidden field
+ )(width := 1.px, height := 1.px, minHeight := 1.px, padding := 0, border := 0, float.left,WidgetUtils.toNullable(field.nullable)) //in order to use HTML5 validation we insert an hidden field
override protected def edit(nested:Binding.NestedInterceptor): JsDom.all.Modifier = {
diff --git a/client/src/main/scala/ch/wsl/box/client/views/components/widget/lookup/LookupWidget.scala b/client/src/main/scala/ch/wsl/box/client/views/components/widget/lookup/LookupWidget.scala
index 7e9972999..2e8fa3645 100644
--- a/client/src/main/scala/ch/wsl/box/client/views/components/widget/lookup/LookupWidget.scala
+++ b/client/src/main/scala/ch/wsl/box/client/views/components/widget/lookup/LookupWidget.scala
@@ -93,20 +93,16 @@ trait LookupWidget extends Widget with HasData {
}
- private def fetchRemoteLookup(fieldLookup:JSONFieldLookupRemote)(q: JSONQuery)(implicit ec: ExecutionContext):Future[Seq[JSONLookup]] = {
+ private def fetchRemoteLookup(fieldLookup:JSONFieldLookupRemote,force:Boolean)(q: JSONQuery)(implicit ec: ExecutionContext):Future[Seq[JSONLookup]] = {
dataSyncRegistration.foreach(_.cancel())
logger.debug(s"Fetching remote lookup $q")
- val cacheKey = metadata.name + ch.wsl.typings.jsMd5.mod.^(fieldLookup.lookupEntity + fieldLookup.map + q.toString)
-
-
-
+ val cacheKey = metadata.name + ch.wsl.typings.jsMd5.mod.hex(fieldLookup.lookupEntity + fieldLookup.map + q.toString)
LookupWidget.remoteLookup.get(cacheKey) match {
- case Some(value) => value.map(setNewLookup)
+ case Some(value) if !force => value.map(setNewLookup)
case _ => {
_lookup.set(Seq(), true) //reset lookup state
-
val request = for{
lookups <- services.rest.lookup(metadata.kind, services.clientSession.lang(), metadata.name, field.name, q, public)
singleLookup <- if(data.get != Json.Null && !lookups.exists(_.id == data.get)) {
@@ -140,11 +136,11 @@ trait LookupWidget extends Widget with HasData {
case Some(query) => {
autoRelease(allData.listen({ allJs =>
val newQuery = query.withData(allJs,services.clientSession.lang())
- fetchRemoteLookup(fieldLookup)(newQuery)
+ fetchRemoteLookup(fieldLookup,force = false)(newQuery)
}, true))
}
- case None => fetchRemoteLookup(fieldLookup)(JSONQuery.empty.limit(1000))
+ case None => fetchRemoteLookup(fieldLookup,force = false)(JSONQuery.empty.limit(1000))
}
@@ -201,16 +197,16 @@ trait LookupWidget extends Widget with HasData {
}
- private def fetchLookups()(implicit ec: ExecutionContext):Future[Seq[JSONLookup]] = {
+ protected def fetchLookups(force:Boolean = false)(implicit ec: ExecutionContext):Future[Seq[JSONLookup]] = {
fieldLookup match {
case r:JSONFieldLookupRemote => {
r.lookupQuery.flatMap(JSONQuery.fromJson) match {
case Some(query) => {
val newQuery = query.withData(allData.get,services.clientSession.lang())
- fetchRemoteLookup(r)(newQuery)(ec)
+ fetchRemoteLookup(r,force)(newQuery)(ec)
}
- case None => fetchRemoteLookup(r)(JSONQuery.empty.limit(1000))(ec)
+ case None => fetchRemoteLookup(r,force)(JSONQuery.empty.limit(1000))(ec)
}
}
case JSONFieldLookupExtractor(extractor) => Future.successful(
diff --git a/client/src/main/scala/ch/wsl/box/client/views/components/widget/lookup/PopupSelectWidget.scala b/client/src/main/scala/ch/wsl/box/client/views/components/widget/lookup/PopupSelectWidget.scala
index ee7bcdacc..c8cb0967a 100644
--- a/client/src/main/scala/ch/wsl/box/client/views/components/widget/lookup/PopupSelectWidget.scala
+++ b/client/src/main/scala/ch/wsl/box/client/views/components/widget/lookup/PopupSelectWidget.scala
@@ -1,9 +1,11 @@
package ch.wsl.box.client.views.components.widget.lookup
+import ch.wsl.box.client.Context.services
import ch.wsl.box.client.services.{BrowserConsole, ClientConf, Labels}
-import ch.wsl.box.client.styles.BootstrapCol
+import ch.wsl.box.client.styles.{BootstrapCol, Icons}
import ch.wsl.box.client.utils.TestHooks
-import ch.wsl.box.client.views.components.widget.{ComponentWidgetFactory, Widget, WidgetParams, WidgetUtils}
+import ch.wsl.box.client.views.components.JSONMetadataRenderer
+import ch.wsl.box.client.views.components.widget.{ComponentWidgetFactory, Widget, WidgetCallbackActions, WidgetParams, WidgetUtils}
import ch.wsl.box.model.shared._
import ch.wsl.box.shared.utils.JSONUtils.EnhancedJson
import io.circe._
@@ -15,6 +17,7 @@ import io.udash.bootstrap.button.UdashButton
import io.udash.bootstrap.modal.UdashModal
import io.udash.bootstrap.modal.UdashModal.ModalEvent
import io.udash.bootstrap.utils.BootstrapStyles.Size
+import io.udash.properties.seq
import io.udash.properties.single.Property
import org.scalajs.dom.{Event, HTMLInputElement, document}
import scalatags.JsDom
@@ -23,6 +26,9 @@ import scribe.Logging
import scala.concurrent.duration._
+sealed trait Mode
+case object Edit extends Mode
+case object Search extends Mode
object PopupSelectWidget extends ComponentWidgetFactory {
@@ -53,6 +59,16 @@ object PopupSelectWidget extends ComponentWidgetFactory {
val Open = "open"
}
+ import ch.wsl.box.client.Context.Implicits._
+
+
+ val mode:Property[Mode] = Property(Search)
+ val enableEdit = field.params.exists(_.jsOpt("enableEdit").exists(_ == Json.True))
+ val childMetadata:Property[Option[JSONMetadata]] = Property(None)
+ val lookupData = Property(Json.obj())
+
+ val editForm:Option[String] = field.params.flatMap(_.getOpt("editForm"))
+
def popupEdit(nested:Binding.NestedInterceptor)(mainRenderer:(UdashModal,Property[String]) => Modifier) = {
val searchId = TestHooks.popupSearch(field.name,metadata.objId)
@@ -64,23 +80,78 @@ object PopupSelectWidget extends ComponentWidgetFactory {
def optionList(nested:NestedInterceptor):Modifier = div(
label(Labels.popup.search),br,
TextInput(searchProp,500.milliseconds)(width := 100.pct, id := searchId),br,br,
+ if(enableEdit) {
+ button(`type` := "button", Icons.plus, " ", Labels.entities.`new`, ClientConf.style.boxButtonImportant, onclick :+= ((e: Event) => {
+ lookupData.set(Json.obj())
+ mode.set(Edit)
+ }))
+ } else Seq[Modifier](),br,br,
nested(showIf(modalStatus.transform(_ == Status.Open)) {
div(ClientConf.style.popupEntiresList,nested(produce(searchProp) { searchTerm =>
div(
div(
nested(repeat(lookup.filter(opt => searchTerm == "" || opt.value.toLowerCase.contains(searchTerm.toLowerCase))) { x =>
- div(a(bind(x.transform(_.value)), onclick :+= ((e: Event) => {
- modalStatus.set(Status.Closed)
- model.set(Some(x.get))
- e.preventDefault()
- }))).render
+ div(ClientConf.style.popupEntiresItem,
+ a(bind(x.transform(_.value)), onclick :+= ((e: Event) => {
+ modalStatus.set(Status.Closed)
+ model.set(Some(x.get))
+ e.preventDefault()
+ })),
+ if(enableEdit) {
+ a(marginRight := 15.px, Icons.pencil_square, onclick :+= ((e: Event) => {
+ val lookup = fieldLookup.asInstanceOf[JSONFieldLookupRemote]
+ val keys = Seq((lookup.map.foreign.keyColumns.head, x.get.id)) // single lookup supported
+ //val keys = lookup.map.localKeysColumn.zip(lookup.map.foreign.keyColumns).map{ case (local,remote) => remote -> x.get.id}
+
+ val data = editForm match {
+ case Some(value) => services.rest.get(EntityKind.FORM.kind, services.clientSession.lang(), value, JSONID.fromMap(keys), public)
+ case None => services.rest.get(EntityKind.ENTITY.kind, services.clientSession.lang(), lookup.lookupEntity, JSONID.fromMap(keys), public)
+ }
+
+ data.map { record =>
+ lookupData.set(record)
+ mode.set(Edit)
+ }
+ e.preventDefault()
+ }))
+ } else Seq[Modifier]()
+ ).render
}
)
)
).render
})).render
}
- ))
+ ))
+
+ def editEntity(entity:String,nested:NestedInterceptor):Modifier = {
+
+
+
+
+
+ logger.debug("Loading child metadata")
+ editForm match {
+ case Some(value) => services.rest.metadata(EntityKind.FORM.kind,services.clientSession.lang(),value,public).foreach(m => childMetadata.set(Some(m)))
+ case None => services.rest.metadata(EntityKind.ENTITY.kind,services.clientSession.lang(),entity,public).foreach(m => childMetadata.set(Some(m)))
+ }
+
+ nested(produceWithNested(childMetadata) { (metadata,nested) =>
+ div(metadata.map { metadata =>
+ val id = JSONID.fromData(lookupData.get, metadata)
+ val action = WidgetCallbackActions.noAction
+ JSONMetadataRenderer(metadata, lookupData, Seq(), Property(id.map(_.asString)), action, Property(false), public).edit(nested)
+ }).render
+ })
+ }
+
+ def editEntry(nested:NestedInterceptor):Modifier = {
+ fieldLookup match {
+ case JSONFieldLookupRemote(lookupEntity, map, lookupQuery) => editEntity(lookupEntity,nested)
+ case JSONFieldLookupExtractor(extractor) => ???
+ case JSONFieldLookupData(data) => ???
+ }
+ }
var modal:UdashModal = null
@@ -94,23 +165,67 @@ object PopupSelectWidget extends ComponentWidgetFactory {
).render
val body = (x:NestedInterceptor) => div(
- div(
- optionList(x)
- )
+ x(produceWithNested(mode) { (m,nested) =>
+ m match {
+ case Edit => div(editEntry(nested)).render
+ case Search => div(optionList(nested)).render
+ }
+ })
).render
- val footer = (nested:NestedInterceptor) => div(
- nested(showIf(model.transform(_.isDefined)) {
- button(onclick :+= ((e: Event) => {
- model.set(None)
- modal.hide()
- e.preventDefault()
- }), Labels.popup.remove, ClientConf.style.boxButtonDanger).render
- }),
- button(onclick :+= ((e:Event) => {
- modal.hide()
- e.preventDefault()
- }), Labels.popup.close,ClientConf.style.boxButton)
+ val footer = (n:NestedInterceptor) => div(
+ n(
+ produceWithNested(mode) { (m,nested) =>
+ m match {
+ case Edit => div(
+ button(onclick :+= ((e:Event) => {
+ mode.set(Search)
+ e.preventDefault()
+ }), Labels.popup.back,ClientConf.style.boxButton),
+ button(onclick :+= ((e:Event) => {
+ val id = JSONID.fromData(lookupData.get,childMetadata.get.get).get
+ val f = editForm match {
+ case Some(value) => services.rest.update(EntityKind.FORM.kind,services.clientSession.lang(),value,id,lookupData.get,public)
+ case None => services.rest.update(EntityKind.ENTITY.kind,services.clientSession.lang(),childMetadata.get.get.entity,id,lookupData.get,public)
+ }
+
+ f.foreach{ _ =>
+ fieldLookup match {
+ case JSONFieldLookupRemote(lookupEntity, map, lookupQuery) => {
+ val id = lookupData.get.js(map.foreign.keyColumns.head)
+ val values = map.foreign.labelColumns.flatMap(x => lookupData.get.getOpt(x))
+ model.set(Some(JSONLookup(id,values)))
+ modal.hide()
+ mode.set(Search)
+ fetchLookups(true)
+ }
+ case JSONFieldLookupExtractor(extractor) => ???
+ case JSONFieldLookupData(data) => ???
+ }
+
+ }
+
+ e.preventDefault()
+ }), Labels.form.save,ClientConf.style.boxButton)
+ ).render
+ case Search => div(
+ nested(showIf(model.transform(_.isDefined)) {
+ button(onclick :+= ((e: Event) => {
+ model.set(None)
+ modal.hide()
+ e.preventDefault()
+ }), Labels.popup.remove, ClientConf.style.boxButtonDanger).render
+ }),
+ button(onclick :+= ((e:Event) => {
+ modal.hide()
+ e.preventDefault()
+ }), Labels.popup.close,ClientConf.style.boxButton)
+ ).render
+
+ }
+ }
+ ),
+
).render
modal = nested(UdashModal(modalSize = Some(Size.Small).toProperty)(
@@ -124,7 +239,10 @@ object PopupSelectWidget extends ComponentWidgetFactory {
modal.listen { case ev:ModalEvent =>
ev.tpe match {
case ModalEvent.EventType.Hide | ModalEvent.EventType.Hidden => modalStatus.set(Status.Closed)
- case ModalEvent.EventType.Shown => document.getElementById(searchId).asInstanceOf[HTMLInputElement].focus()
+ case ModalEvent.EventType.Shown => {
+ mode.set(Search)
+ document.getElementById(searchId).asInstanceOf[HTMLInputElement].focus()
+ }
case _ => {}
}
}
@@ -157,11 +275,10 @@ object PopupSelectWidget extends ComponentWidgetFactory {
override def edit(nested:Binding.NestedInterceptor): JsDom.all.Modifier = popupEdit(nested)((modal,modalStatus) => {
val tooltip = WidgetUtils.addTooltip(field.tooltip) _
- div(BootstrapCol.md(12),ClientConf.style.noPadding, ClientConf.style.smallBottomMargin,
- BootstrapStyles.Display.flex(),BootstrapStyles.Flex.justifyContent(BootstrapStyles.FlexContentJustification.Between))(
+ div(BootstrapCol.md(12),ClientConf.style.noPadding, ClientConf.style.smallBottomMargin,BootstrapStyles.Float.right())(
WidgetUtils.toLabel(field,WidgetUtils.LabelRight),
TextInput(data.bitransform(_.string)(x => data.get))(width := 1.px, height := 1.px, padding := 0, border := 0, float.left,WidgetUtils.toNullable(field.nullable)), //in order to use HTML5 validation we insert an hidden field
- tooltip(button(ClientConf.style.popupButton, onclick :+= ((e:Event) => {
+ tooltip(button(ClientConf.style.popupButton,BootstrapStyles.Float.right(), onclick :+= ((e:Event) => {
modalStatus.set(Status.Open)
e.preventDefault()
}),nested(bind(model.transform(_.map(_.value).getOrElse(""))))).render)._1,
diff --git a/client/src/main/scala/io/udash/routing/BoxUrlChangeProvider.scala b/client/src/main/scala/io/udash/routing/BoxUrlChangeProvider.scala
index 1fba3788d..ec0b60604 100644
--- a/client/src/main/scala/io/udash/routing/BoxUrlChangeProvider.scala
+++ b/client/src/main/scala/io/udash/routing/BoxUrlChangeProvider.scala
@@ -1,7 +1,7 @@
package io.udash.routing
import ch.wsl.box.client.Context
-import ch.wsl.box.client.services.{BrowserConsole, Labels, Navigate}
+import ch.wsl.box.client.services.{BrowserConsole, ClientConf, Labels, Navigate}
import com.avsystem.commons._
import io.udash.core.Url
import io.udash.properties.MutableSetRegistration
@@ -92,7 +92,7 @@ final class BoxUrlChangeProvider extends UrlChangeProvider with Logging {
}
private def baseUri = {
- val bu = dom.document.baseURI
+ val bu = ClientConf.frontendUrl
logger.debug(s"Base URI with dom.document.baseURI: $bu")
if(bu.startsWith("file:///")) {
bu.split("html").headOption.map( x => s"${x}html").getOrElse(bu)
diff --git a/client/src/test/scala/ch/wsl/box/client/mocks/HttpClientMock.scala b/client/src/test/scala/ch/wsl/box/client/mocks/HttpClientMock.scala
index d52a21ce9..7da3efeab 100644
--- a/client/src/test/scala/ch/wsl/box/client/mocks/HttpClientMock.scala
+++ b/client/src/test/scala/ch/wsl/box/client/mocks/HttpClientMock.scala
@@ -2,7 +2,7 @@ package ch.wsl.box.client.mocks
import ch.wsl.box.client.services.HttpClient
import io.circe.{Decoder, Encoder}
-import org.scalajs.dom.File
+import org.scalajs.dom.{Blob, File}
import scala.concurrent.{ExecutionContext, Future}
import scala.scalajs.js
@@ -25,4 +25,6 @@ class HttpClientMock extends HttpClient {
override def sendRaw[T](url: String, data: js.Any)(implicit decoder: Decoder[T], ex: ExecutionContext): Future[T] = ???
override def setHandleAuthFailure(f: () => Unit): Unit = {}
+
+ override def getBlob(url: String)(implicit ex: ExecutionContext): Future[Blob] = ???
}
diff --git a/client/src/test/scala/ch/wsl/box/client/mocks/RestMock.scala b/client/src/test/scala/ch/wsl/box/client/mocks/RestMock.scala
index fa5b5baf4..509201089 100644
--- a/client/src/test/scala/ch/wsl/box/client/mocks/RestMock.scala
+++ b/client/src/test/scala/ch/wsl/box/client/mocks/RestMock.scala
@@ -142,7 +142,7 @@ class RestMock(values:Values) extends REST with Logging {
override def importXLS(kind: String, lang: String, entity: String, data: File)(implicit ec:ExecutionContext): Future[Int] = ???
- val userInfo = UserInfo("t","t",None,Seq(),Json.Null)
+ val userInfo = UserInfo("t","t","t",None,Seq(),Json.Null)
override def login(request: LoginRequest)(implicit ec:ExecutionContext): Future[UserInfo] = Future.successful{
userInfo
}
diff --git a/client/style.scss b/client/style.scss
new file mode 100644
index 000000000..e69de29bb
diff --git a/client/vite.config.app.js b/client/vite.config.app.js
new file mode 100644
index 000000000..ef5f3213a
--- /dev/null
+++ b/client/vite.config.app.js
@@ -0,0 +1,68 @@
+import { defineConfig } from "vite";
+import scalaJSPlugin from "@scala-js/vite-plugin-scalajs";
+import { loadEnv } from 'vite';
+import { viteStaticCopy } from 'vite-plugin-static-copy';
+
+const env = loadEnv(process.env.NODE_ENV, process.cwd());
+
+export default defineConfig({
+ base: './',
+ server: {
+ proxy: {
+ '/api': 'http://localhost:8080',
+ '/pdf': 'http://localhost:8080',
+ '/ui/workers': {
+ target: 'http://127.0.0.1:5174',
+ changeOrigin: true,
+ rewrite: (path) => {
+ console.log(path)
+ return path.replace(/^\/ui\/workers/, '')
+ },
+ }
+ },
+ cors: true,
+ host: '0.0.0.0'
+ },
+ optimizeDeps: {
+ exclude: ['@electric-sql/pglite'],
+ },
+ resolve: {
+ alias: {
+ '~bootstrap': './node_modules/bootstrap',
+ }
+ },
+ plugins: [
+ scalaJSPlugin({
+ // path to the directory containing the sbt build
+ // default: '.'
+ cwd: '..',
+
+ // sbt project ID from within the sbt build to get fast/fullLinkJS from
+ // default: the root project of the sbt build
+ projectID: 'client',
+
+ // URI prefix of imports that this plugin catches (without the trailing ':')
+ // default: 'scalajs' (so the plugin recognizes URIs starting with 'scalajs:')
+ uriPrefix: 'scalajs',
+ }),
+ viteStaticCopy({
+ targets: [
+ {
+ src: './node_modules/@electric-sql/pglite-repl/dist-webcomponent/*',
+ dest: 'public',
+ rename: { stripBase: 1 }
+ }
+ ]
+ })
+ ],
+ build: {
+ emptyOutDir: false,
+ rollupOptions: {
+ output: {
+ entryFileNames: `ui/[name].${env.VITE_BOX_VERSION}.js`,
+ chunkFileNames: `ui/[name].${env.VITE_BOX_VERSION}.js`,
+ assetFileNames: `ui/[name].${env.VITE_BOX_VERSION}.[ext]`,
+ },
+ },
+ },
+});
\ No newline at end of file
diff --git a/client/vite.config.worker.js b/client/vite.config.worker.js
new file mode 100644
index 000000000..8dbdaa6f4
--- /dev/null
+++ b/client/vite.config.worker.js
@@ -0,0 +1,26 @@
+import { defineConfig } from 'vite'
+import { loadEnv } from 'vite';
+
+const env = loadEnv(process.env.NODE_ENV, process.cwd());
+
+export default defineConfig({
+ optimizeDeps: {
+ exclude: ['@electric-sql/pglite'],
+ },
+ build: {
+ outDir: './dist',
+ //watch: {},
+ emptyOutDir: false,
+ rollupOptions: {
+ input: ['./workers/postgres.worker.js','./workers/sw.js'],
+ // externalize the package so Rollup doesn't bundle its JS/WASM
+ output: {
+ // keep emitted asset names predictable
+ entryFileNames: `ui/workers/[name].${env.VITE_BOX_VERSION}.js`,
+ chunkFileNames: `ui/workers/[name].${env.VITE_BOX_VERSION}.js`,
+ assetFileNames: `ui/workers/[name].${env.VITE_BOX_VERSION}.[ext]`,
+ },
+ }
+
+ },
+})
diff --git a/client/workers/postgres.worker.js b/client/workers/postgres.worker.js
new file mode 100644
index 000000000..bac02fa81
--- /dev/null
+++ b/client/workers/postgres.worker.js
@@ -0,0 +1,30 @@
+import { PGlite } from '@electric-sql/pglite'
+import { worker } from '@electric-sql/pglite/worker'
+
+console.log('Worker started');
+try {
+ worker({
+ async init() {
+ const params = new URLSearchParams(location.search)
+ const version = params.get("version")
+ const id = params.get("appId")
+
+ const [pgliteWasmModule, initdbWasmModule, fsBundle] = await Promise.all([
+ WebAssembly.compileStreaming(fetch(`./pglite.${version}.wasm`)),
+ WebAssembly.compileStreaming(fetch(`./initdb.${version}.wasm`)),
+ fetch(`./pglite.${version}.data`).then((response) => response.blob()),
+ ])
+ // Create and return a PGlite instance
+ return PGlite.create(`idb://box-pgdata-${id}`,{
+ pgliteWasmModule: pgliteWasmModule,
+ initdbWasmModule: initdbWasmModule,
+ fsBundle: fsBundle
+ })
+ },
+ })
+} catch (error) {
+ console.error('Error starting worker:', error);
+}
+self.onmessage = function(event) {
+ console.log('Message received in worker:', event.data);
+};
diff --git a/resources/sw.js b/client/workers/sw.js
similarity index 100%
rename from resources/sw.js
rename to client/workers/sw.js
diff --git a/codegen/src/main/scala/ch/wsl/box/jdbc/Connection.scala b/codegen/src/main/scala/ch/wsl/box/jdbc/Connection.scala
index d0d7c7a28..a3411c4d7 100644
--- a/codegen/src/main/scala/ch/wsl/box/jdbc/Connection.scala
+++ b/codegen/src/main/scala/ch/wsl/box/jdbc/Connection.scala
@@ -20,6 +20,8 @@ import org.postgresql.ds.PGSimpleDataSource
import java.util.UUID
import scala.collection.JavaConverters._
import scala.concurrent.{Await, ExecutionContext}
+import cats.effect._
+import skunk.{Session, _}
/**
* Created by andreaminetti on 16/02/16.
@@ -37,6 +39,9 @@ trait Connection extends Logging {
//val executor = AsyncExecutor("public-executor",50,50,10000,50)
+ def notificationSession():Resource[IO, skunk.Session[IO]]
+ def pooledAdminSession(): Resource[IO, Resource[IO, Session[IO]]]
+
def adminDB = dbForUser(adminUser,"box_admin",adminDbConnection)
@@ -93,8 +98,26 @@ class ConnectionConfImpl extends Connection {
ConfigValueFactory.fromAnyRef("disabled")
}
+ import natchez.Trace.Implicits.noop
+
+ private val pgConf = JdbcParser.parse(dbPath).get
+ override def notificationSession(): Resource[IO, Session[IO]] = Session.single[IO](
+ host=pgConf.host,
+ port= pgConf.port,
+ user=adminUser,
+ database = pgConf.database,
+ password = Some(dbPassword)
+ )
+ override def pooledAdminSession(): Resource[IO, Resource[IO, Session[IO]]] = Session.pooled[IO](
+ host=pgConf.host,
+ port= pgConf.port,
+ user=adminUser,
+ database = pgConf.database,
+ password = Some(dbPassword),
+ max = 2
+ )
println(s"DB: $dbPath")
@@ -169,10 +192,11 @@ class ConnectionTestContainerImpl(container: PostgreSQLContainer,schema:String)
val idleTimeout = 300000
+ override def notificationSession(): Resource[IO, Session[IO]] = ???
+ override def pooledAdminSession(): Resource[IO, Resource[IO, Session[IO]]] = ???
-
- override def dataSource(name:String,schema:String): DataSource = {
+ override def dataSource(name:String, schema:String): DataSource = {
val ds = new PGSimpleDataSource()
ds.setUrl(dbPath)
ds.setUser(adminUser)
diff --git a/codegen/src/main/scala/ch/wsl/box/jdbc/PostgresConfig.scala b/codegen/src/main/scala/ch/wsl/box/jdbc/PostgresConfig.scala
new file mode 100644
index 000000000..3bb5b96e6
--- /dev/null
+++ b/codegen/src/main/scala/ch/wsl/box/jdbc/PostgresConfig.scala
@@ -0,0 +1,31 @@
+package ch.wsl.box.jdbc
+
+
+case class PostgresConfig(
+ host: String,
+ port: Int,
+ database: String,
+ params: Map[String, String]
+ )
+
+object JdbcParser {
+ // Regex for jdbc:postgresql://[host][:port]/[database][?params]
+ private val JdbcRegex = """jdbc:postgresql://([^:/]+)(?::(\d+))?/([^?]+)(?:\?(.*))?""".r
+
+ def parse(url: String): Option[PostgresConfig] = url match {
+ case JdbcRegex(host, port, db, params) =>
+ val portNum = Option(port).map(_.toInt).getOrElse(5432)
+ val paramMap = Option(params).toSeq
+ .flatMap(_.split("&"))
+ .flatMap { pair =>
+ pair.split("=", 2) match {
+ case Array(k, v) => Some(k -> v)
+ case _ => None
+ }
+ }.toMap
+
+ Some(PostgresConfig(host, portNum, db, paramMap))
+
+ case _ => None
+ }
+}
\ No newline at end of file
diff --git a/db/box_migrations/BOX_R__v_jsonschema.sql b/db/box_migrations/BOX_R__v_jsonschema.sql
new file mode 100644
index 000000000..00995e796
--- /dev/null
+++ b/db/box_migrations/BOX_R__v_jsonschema.sql
@@ -0,0 +1,58 @@
+drop view if exists v_jsonschema;
+create or replace view v_jsonschema as
+WITH cols AS not materialized (
+ SELECT
+ column_name,
+ data_type,
+ is_nullable,
+ character_maximum_length,
+ numeric_precision,
+ numeric_scale,
+ column_default,
+ table_schema,
+ table_name,
+ jsonb_strip_nulls(jsonb_build_object(
+ 'type', CASE
+ WHEN data_type IN ('integer','bigint','smallint') THEN '"integer"'::jsonb
+ WHEN data_type IN ('numeric','decimal','real','double precision') THEN '"number"'::jsonb
+ WHEN data_type = 'boolean' THEN '"boolean"'::jsonb
+ WHEN data_type LIKE '%char%' OR data_type = 'text' THEN '"string"'::jsonb
+ WHEN data_type = 'date' THEN '"string"'::jsonb
+ WHEN data_type LIKE 'timestamp%' THEN '"string"'::jsonb
+ WHEN data_type IN ('json','jsonb') THEN '"object"'::jsonb
+ ELSE '"string"'::jsonb END,
+ 'format', case
+ WHEN data_type = 'date' THEN '"date"'::jsonb
+ WHEN data_type LIKE 'timestamp%' THEN '"date-time"'::jsonb
+ else null::jsonb end,
+ 'nullable', (is_nullable = 'YES'),
+ 'maxLength', CASE
+ WHEN character_maximum_length IS NOT NULL THEN to_jsonb(character_maximum_length)
+ ELSE NULL
+ END,
+ 'precision', CASE
+ WHEN numeric_precision IS NOT NULL THEN to_jsonb(numeric_precision)
+ ELSE NULL
+ END,
+ 'scale', CASE
+ WHEN numeric_scale IS NOT NULL THEN to_jsonb(numeric_scale)
+ ELSE NULL
+ END,
+ 'default', to_jsonb(column_default)
+ )) as column_jsonschema
+ FROM information_schema.columns
+)
+SELECT table_schema::text,table_name::text,jsonb_build_object(
+ '$schema', 'https://json-schema.org/draft/2020-12/schema',
+ '$id', to_jsonb('urn:postgres:' || table_schema || ':' || table_name),
+ 'title', to_jsonb(table_name),
+ 'type', 'object',
+ 'properties', jsonb_object_agg(column_name,column_jsonschema),
+ 'required', (SELECT jsonb_agg(column_name)
+ FROM cols c
+ WHERE c.is_nullable = 'NO' and c.table_schema = cols.table_schema and c.table_name=cols.table_name)
+ )::jsonb AS json_schema
+FROM cols
+group by table_schema, table_name;
+
+grant select on v_jsonschema to box_user;
diff --git a/db/box_migrations/functions/BOX_R__box_notify.sql b/db/box_migrations/functions/BOX_R__box_notify.sql
new file mode 100644
index 000000000..cf18979f7
--- /dev/null
+++ b/db/box_migrations/functions/BOX_R__box_notify.sql
@@ -0,0 +1,18 @@
+create or replace function box_notify(
+ channel text,
+ payload jsonb
+)
+ returns void set search_path from current as $$
+declare
+ output text := '';
+begin
+
+ output := to_jsonb(
+ (SELECT t FROM (SELECT channel,payload) AS t (channel,payload))
+ )::text;
+
+ -- subtracting the amount from the sender's account
+ PERFORM pg_notify('box_' || current_schema || '_channel',output);
+end;
+$$ LANGUAGE plpgsql;
+
diff --git a/db/box_migrations/functions/BOX_R__mail_notifications.sql b/db/box_migrations/functions/BOX_R__mail_notifications.sql
new file mode 100644
index 000000000..6d30eea84
--- /dev/null
+++ b/db/box_migrations/functions/BOX_R__mail_notifications.sql
@@ -0,0 +1,54 @@
+create or replace function mail_notification(_mail_from email, _mail_reply_to email, _mail_to email[], _mail_cc email[], _mail_bcc email[], _subject text, _text text, _html text, _params jsonb) returns uuid
+ SET search_path from current
+ language plpgsql
+as
+$$
+declare
+ _mail_id uuid;
+begin
+
+ insert into mails (send_at,wished_send_at,mail_from,mail_to,mail_cc,mail_bcc,subject,text,html,params,created,reply_to) values
+ (now(),now(),_mail_from, _mail_to, _mail_cc,_mail_bcc, _subject, _text, _html,_params, now(),_mail_reply_to) returning id into _mail_id;
+
+ -- subtracting the amount from the sender's account
+ PERFORM box_notify('mail_feedback_channel','{"sendMail": true}'::jsonb);
+
+ return _mail_id;
+end;
+$$;
+
+
+create or replace function mail_notification(_mail_from email, _mail_to email[], _mail_cc email[], _mail_bcc email[], _subject text, _text text, _html text, _params jsonb) returns uuid
+ SET search_path from current
+ language sql
+as
+$$
+
+select mail_notification(_mail_from, null::email, _mail_to, _mail_cc, _mail_bcc, _subject, _text, _html, _params);
+
+$$;
+
+
+
+
+
+create or replace function mail_notification(_mail_from email, _mail_to email[], _subject text, _text text, _html text, _params jsonb) returns uuid
+ language sql
+ set search_path from current
+as
+$$
+select mail_notification(_mail_from,_mail_to,array[]::email[],array[]::email[],_subject,_text,_html,_params);
+$$;
+
+
+create or replace function mail_notification(_mail_from email, _mail_to email[], _subject text, _text text, _html text) returns uuid
+ language sql
+ set search_path from current
+as
+$$
+select mail_notification(_mail_from,_mail_to,_subject,_text,_html,'{}'::jsonb);
+$$;
+
+
+
+
diff --git a/db/box_migrations/functions/BOX_R__translate_column.sql b/db/box_migrations/functions/BOX_R__translate_column.sql
new file mode 100644
index 000000000..2a6faacbd
--- /dev/null
+++ b/db/box_migrations/functions/BOX_R__translate_column.sql
@@ -0,0 +1,16 @@
+create or replace function translate_column(schema text, _table text, from_lang text, to_lang text, from_column text, to_column text) returns boolean
+ SET search_path from current
+ language plpgsql as $$
+begin
+ perform box_notify('translate_column',
+ jsonb_build_object(
+ 'schema',schema,
+ 'table', _table,
+ 'from_lang', from_lang,
+ 'to_lang',to_lang,
+ 'from_column', from_column,
+ 'to_column',to_column
+ ));
+ return true;
+end;
+$$;
\ No newline at end of file
diff --git a/db/box_migrations/functions/BOX_R__ui_notification.sql b/db/box_migrations/functions/BOX_R__ui_notification.sql
new file mode 100644
index 000000000..3d9391330
--- /dev/null
+++ b/db/box_migrations/functions/BOX_R__ui_notification.sql
@@ -0,0 +1,28 @@
+create or replace function ui_notification(topic text, users text[], payload json) returns void
+ language plpgsql
+ set search_path from current
+as
+$$
+declare
+ output text := '';
+begin
+
+ output := row_to_jsonb(
+ (SELECT ColumnName FROM (SELECT topic,users,payload) AS ColumnName (topic,allowed_users,payload))
+ )::text;
+
+ -- subtracting the amount from the sender's account
+ PERFORM box_notify('ui_feedback_channel',output);
+end;
+$$;
+
+
+create or replace function ui_notification_forall(topic text, payload json) returns void
+ language plpgsql
+ set search_path from current
+as
+$$
+begin
+ PERFORM ui_notification(topic,'{"ALL_USERS"}'::text[],payload);
+end;
+$$;
\ No newline at end of file
diff --git a/project/BoxScalablyTypes.scala b/project/BoxScalablyTypes.scala
new file mode 100644
index 000000000..fb08f5fec
--- /dev/null
+++ b/project/BoxScalablyTypes.scala
@@ -0,0 +1,78 @@
+import com.olvind.logging
+import com.olvind.logging.LogLevel
+import org.scalablytyped.converter.{Flavour, Selection}
+import org.scalablytyped.converter.internal.{BuildInfo, IArray, ImportTypingsGenSources, InFolder, Json, files}
+import org.scalablytyped.converter.internal.ImportTypingsGenSources.Input
+import org.scalablytyped.converter.internal.importer.{ConversionOptions, EnabledTypeMappingExpansion}
+import org.scalablytyped.converter.internal.scalajs.{Name, QualifiedName, Versions}
+import org.scalablytyped.converter.internal.ts.{PackageJson, TsIdentLibrary}
+import os.Path
+import sbt.*
+
+import scala.collection.immutable.{SortedMap, SortedSet}
+
+object BoxScalablyTypes {
+
+ val outputName = Name("ch.wsl.typings")
+
+
+
+
+ val conversion = ConversionOptions(
+ useScalaJsDomTypes = true,
+ flavour = Flavour.Slinky,
+ outputPackage = outputName,
+ enableScalaJsDefined = Selection.All,
+ stdLibs = SortedSet("es6", "es2018.asyncgenerator"),
+ expandTypeMappings = EnabledTypeMappingExpansion.DefaultSelection,
+ ignored = SortedSet(
+ "@fontsource/open-sans",
+ "redux",
+ "node",
+ "crypto-browserify",
+ "ol-ext",
+ "@fortawesome/fontawesome-free",
+ "stream-browserify",
+ "toolcool-range-slider",
+ "@tailwindcss/vite",
+ "string-strip-html",
+ "autocompleter",
+ ),
+ versions = Versions(Versions.Scala213, Versions.ScalaJs1),
+ organization = "org.scalablytyped",
+ enableReactTreeShaking = Selection.None,
+ enableLongApplyMethod = false,
+ privateWithin = None,
+ useDeprecatedModuleNames = false,
+ )
+
+
+
+ def generateSJSFromTS(_clientPath: SettingKey[_root_.java.io.File]) = Def.task {
+ val clientPath = os.Path(_clientPath.value)
+
+ val libs = Json.force[PackageJson](clientPath / "package.json").allLibs(dev = false, peer = false)
+
+ println(libs)
+
+ ImportTypingsGenSources(
+ input = Input(
+ fromFolder = InFolder(clientPath / "node_modules"),
+ targetFolder = files.existing(clientPath / "target" / "scala-2.13" / "src_scalablytypes" / "main"),
+ overrideTargetFolder = Map.empty,
+ converterVersion = BuildInfo.version,
+ conversion = conversion,
+ wantedLibs = libs,
+ minimize = Selection.NoneExcept(TsIdentLibrary("std")),
+ minimizeKeep = IArray(),
+ ),
+ logger = logging.stdout.filter(LogLevel.warn),
+ parseCacheDirOpt = None,
+ cacheDirOpt = clientPath,
+ ).toOption.toSeq.flatten
+
+ }
+
+
+
+}
\ No newline at end of file
diff --git a/project/Settings.scala b/project/Settings.scala
index b82389888..d2aa2e4b0 100755
--- a/project/Settings.scala
+++ b/project/Settings.scala
@@ -32,7 +32,7 @@ object Settings {
object versions {
//General
- val scala213 = "2.13.16"
+ val scala213 = "2.13.18"
val ficus = "1.5.2"
val macWire = "2.3.7"
@@ -52,7 +52,7 @@ object Settings {
//json parsers
- val circe = "0.14.3"
+ val circe = "0.14.15"
//database
val postgres = "42.2.20"
@@ -66,7 +66,7 @@ object Settings {
//js
val bootstrap = "3.4.1-1"
- val udash = "0.9.0-M39"
+ val udash = "0.22.0"
val udashJQuery = "3.0.4"
val scribe = "3.0.2"
@@ -88,7 +88,7 @@ object Settings {
"io.circe" %%% "circe-core" % versions.circe,
"io.circe" %%% "circe-generic" % versions.circe,
"io.circe" %%% "circe-parser" % versions.circe,
- "io.circe" %%% "circe-generic-extras" % versions.circe,
+ "io.circe" %%% "circe-generic-extras" % "0.14.4", // not same versioning of circe https://github.com/circe/circe-generic-extras?tab=readme-ov-file#versioning
"com.outr" %%% "scribe" % versions.scribe,
"com.nrinaudo" %%% "kantan.csv" % versions.kantan,
"com.github.eikek" %%% "yamusca-core" % "0.8.0",
@@ -110,6 +110,7 @@ object Settings {
"org.flywaydb" % "flyway-database-postgresql" % versions.flyway,
"com.outr" %% "scribe" % versions.scribe,
"com.outr" %% "scribe-slf4j18" % versions.scribe,
+ "org.tpolecat" %% "skunk-core" % "0.6.5"
))
val codegenDependecies = Def.setting(sharedJVMCodegenDependencies.value ++ Seq(
@@ -202,6 +203,7 @@ object Settings {
"org.http4s" %%% "http4s-dom" % "0.2.3",
"org.http4s" %%% "http4s-client" % "0.23.16",
"org.http4s" %%% "http4s-circe" % "0.23.16",
+ "com.olvind" %%% "scalablytyped-runtime" % "2.4.2"
//"io.github.cquiroz" %%% "scala-java-locales" % "1.5.1",
// "io.github.cquiroz" %%% "scala-java-time-tzdb" % "2.5.0"
))
diff --git a/project/plugins.sbt b/project/plugins.sbt
index b7ab820c8..3f230dfba 100755
--- a/project/plugins.sbt
+++ b/project/plugins.sbt
@@ -1,13 +1,11 @@
//addSbtPlugin("com.vmunier" % "sbt-web-scalajs" % "1.1.0")
-addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.19.0")
+addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.20.2")
-addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "4.0.2")
addSbtPlugin("io.github.cquiroz" % "sbt-locales" % "4.2.0")
addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.9.0")
-addSbtPlugin("io.spray" % "sbt-revolver" % "0.9.1")
addSbtPlugin("com.typesafe.play" % "sbt-twirl" % "1.5.2")
addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.0.0")
@@ -16,7 +14,7 @@ addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.1.2")
addSbtPlugin("com.typesafe.sbt" % "sbt-git" % "1.0.0")
addSbtPlugin("org.scalablytyped.converter" % "sbt-converter" % "1.0.0-beta44")
-addSbtPlugin("ch.epfl.scala" % "sbt-web-scalajs-bundler" % "0.21.0")
+
addDependencyTreePlugin
diff --git a/server-services/src/main/scala/ch/wsl/box/services/config/ConfigFileImpl.scala b/server-services/src/main/scala/ch/wsl/box/services/config/ConfigFileImpl.scala
index 86b1505bd..add2e5dcc 100644
--- a/server-services/src/main/scala/ch/wsl/box/services/config/ConfigFileImpl.scala
+++ b/server-services/src/main/scala/ch/wsl/box/services/config/ConfigFileImpl.scala
@@ -12,5 +12,8 @@ class ConfigFileImpl extends Config {
override def postgisSchemaName: String = conf.as[Option[String]]("db.postgis.schema").getOrElse(schemaName)
override def langs:Seq[String] = conf.as[Option[String]]("langs").map(_.split(",").map(_.trim).toSeq).getOrElse(Seq("en"))
- override def frontendUrl: String = conf.as[String]("box.frontend.url").stripSuffix("/")
+ override def frontendUrl: String = {
+ val url = conf.as[String]("box.frontend.url")
+ if(url.endsWith("/")) url else url + "/"
+ }
}
diff --git a/server/src/main/scala/ch/wsl/box/model/Translations.scala b/server/src/main/scala/ch/wsl/box/model/Translations.scala
index 4dd3699fa..f960c1d36 100644
--- a/server/src/main/scala/ch/wsl/box/model/Translations.scala
+++ b/server/src/main/scala/ch/wsl/box/model/Translations.scala
@@ -3,7 +3,7 @@ package ch.wsl.box.model
import ch.wsl.box.jdbc.UserDatabase
import ch.wsl.box.model.boxentities.{BoxField, BoxForm, BoxLabels}
import ch.wsl.box.jdbc.PostgresProfile.api._
-import ch.wsl.box.model.shared.{BoxTranslationsFields, Field}
+import ch.wsl.box.model.shared.{BoxTranslationField, BoxTranslationsFields, Field}
import ch.wsl.box.services.Services
import java.util.UUID
@@ -184,4 +184,28 @@ object Translations {
}.recover{ case t => t.printStackTrace(); throw t }
}
+
+ def autoTranslate(from:String, to:String, force:Boolean = false)(implicit services: Services, ec:ExecutionContext): Future[Int] = {
+ val c = services.connection.adminDB
+ for {
+ source <- exportFields(from,c)
+ dest <- exportFields(from,c)
+ sourceToTranslate = source.filter(f => force || !dest.exists(d => f.uuid == d.uuid && d.label != ""))
+ translated <- services.translation.translateAll(from,to,sourceToTranslate.map(_.label))
+ result <- updateFields(BoxTranslationsFields(from,to,
+ sourceToTranslate.zip(translated).map{case (src,translated) =>
+ BoxTranslationField(src,src.copy(
+ label = translated,
+ lookup_columns = src.lookup_columns.map{
+ case l if l == from => to
+ case l if l.endsWith(s"_$from") => l.substring(0,l.length - (from.length + 1)) + "_" + to
+ case l => l
+ }
+ ))
+ }
+ ),c)
+ } yield result
+ }
+
+
}
diff --git a/server/src/main/scala/ch/wsl/box/model/UpdateTable.scala b/server/src/main/scala/ch/wsl/box/model/UpdateTable.scala
index 9c205fc5f..e65629e4b 100644
--- a/server/src/main/scala/ch/wsl/box/model/UpdateTable.scala
+++ b/server/src/main/scala/ch/wsl/box/model/UpdateTable.scala
@@ -389,7 +389,7 @@ trait UpdateTable[T] extends BoxTable[T] with Logging { t:Table[T] =>
case ("org.locationtech.jts.geom.Geometry",_) => filter[Geometry](col.nullable,Geo.fromEWKT(v))
case ("java.util.UUID",_) => filter[java.util.UUID](col.nullable,Try(UUID.fromString(v)).toOption)
case ("Boolean",_) => filter[Boolean](col.nullable,Some(v == "true"))
- case t => throw new Exception(s"$t is not supported for simple query")
+ case t => throw new Exception(s"$t is not supported for simple query. On table ${table.tableName} $jsonQuery")
}
}
diff --git a/server/src/main/scala/ch/wsl/box/rest/Boot.scala b/server/src/main/scala/ch/wsl/box/rest/Boot.scala
index dee115f9a..b94597d3d 100755
--- a/server/src/main/scala/ch/wsl/box/rest/Boot.scala
+++ b/server/src/main/scala/ch/wsl/box/rest/Boot.scala
@@ -16,7 +16,7 @@ import scribe.writer.ConsoleWriter
import wvlet.airframe.Design
import ch.wsl.box.model.Migrate
import ch.wsl.box.rest.logic.cron.{BoxCronLoader, CronScheduler}
-import ch.wsl.box.rest.logic.notification.{MailHandler, NotificationsHandler}
+import ch.wsl.box.rest.logic.notification.{MailHandler}
import ch.wsl.box.rest.utils.CertificateUtils
import scala.concurrent.{Await, ExecutionContext, Future, Promise}
@@ -58,7 +58,7 @@ class Box(name:String,version:String,https:Boolean)(implicit services: Services)
//Registring handlers
- new MailHandler(services.mailDispatcher).listen()
+ new MailHandler(services.dbNotify,services.mailDispatcher).listen()
val scheduler = new CronScheduler(system)
new BoxCronLoader(scheduler).load()
@@ -114,7 +114,7 @@ object Boot extends App {
val (name,app_version,https) = args.length match {
case 3 => (args(0),args(1),args(2).toBoolean)
case 2 => (args(0),args(1),false)
- case _ => ("Standalone","DEV",true)
+ case _ => ("Standalone","DEV",false)
}
def run(name:String,app_version:String,module:Design) {
diff --git a/server/src/main/scala/ch/wsl/box/rest/Module.scala b/server/src/main/scala/ch/wsl/box/rest/Module.scala
index 410584a79..cb6831041 100644
--- a/server/src/main/scala/ch/wsl/box/rest/Module.scala
+++ b/server/src/main/scala/ch/wsl/box/rest/Module.scala
@@ -2,14 +2,16 @@ package ch.wsl.box.rest
import akka.actor.ActorSystem
import ch.wsl.box.jdbc.{Connection, ConnectionConfImpl}
+import ch.wsl.box.rest.logic.notification.{DbNotify, DbNotifyHandlerImpl}
import ch.wsl.box.rest.routes.v1.{NotificationChannels, NotificationChannelsImpl}
import ch.wsl.box.rest.utils.BoxSession
import ch.wsl.box.services.{Services, ServicesWithoutGeneration}
-import ch.wsl.box.services.config.{ConfFileAndDb, Config, ConfigFileImpl, FullConfig, FullConfigFileOnlyImpl}
+import ch.wsl.box.services.config.{ConfFileAndDb, Config, ConfigFileImpl, DeepLConfig, FullConfig, FullConfigFileOnlyImpl}
import ch.wsl.box.services.file.ImageCacheStorage
import ch.wsl.box.services.files.{InMemoryImageCacheStorage, PgImageCacheStorage}
import ch.wsl.box.services.mail.{MailService, MailServiceCourier, MailServiceDummy}
import ch.wsl.box.services.mail_dispatcher.{MailDispatcherService, SingleHostMailDispatcherService}
+import ch.wsl.box.services.translation.{ColumnTranslate, DeepLTranslateService, TranslateService}
import com.softwaremill.session.{InMemoryRefreshTokenStorage, RefreshTokenStorage}
import scribe.Logging
import wvlet.airframe._
@@ -45,9 +47,12 @@ object DefaultModule extends Module {
.bind[ImageCacheStorage].to[PgImageCacheStorage]
.bind[MailService].to[MailServiceCourier]
.bind[Connection].to[ConnectionConfImpl]
+ .bind[DbNotify].to[DbNotifyHandlerImpl]
.bind[NotificationChannels].to[NotificationChannelsImpl]
.bind[FullConfig].to[ConfFileAndDb]
.bind[MailDispatcherService].to[SingleHostMailDispatcherService]
+ .bind[DeepLConfig].to[ConfFileAndDb]
+ .bind[TranslateService].to[DeepLTranslateService]
.bind[RefreshTokenStorage[BoxSession]].toInstance(new InMemoryRefreshTokenStorage[BoxSession] with Logging {
override def log(msg: String): Unit = logger.info(msg)
})
@@ -57,6 +62,7 @@ object DefaultModule extends Module {
s.connection.dbConnection.close()
s.connection.adminDbConnection.close()
}
+ .bind[ColumnTranslate].toEagerSingleton
}
diff --git a/server/src/main/scala/ch/wsl/box/rest/auth/oidc/AuthFlow.scala b/server/src/main/scala/ch/wsl/box/rest/auth/oidc/AuthFlow.scala
index bc6ac3343..5ff4b7d85 100644
--- a/server/src/main/scala/ch/wsl/box/rest/auth/oidc/AuthFlow.scala
+++ b/server/src/main/scala/ch/wsl/box/rest/auth/oidc/AuthFlow.scala
@@ -74,7 +74,7 @@ object AuthFlow {
"client_id" -> provider.client_id,
"client_secret" -> provider.client_secret,
"code" -> c,
- "redirect_uri" -> s"${services.config.frontendUrl}/authenticate/${provider.provider_id}"
+ "redirect_uri" -> s"${services.config.frontendUrl}authenticate/${provider.provider_id}"
))
.response(asJson[OpenIDToken])
diff --git a/server/src/main/scala/ch/wsl/box/rest/io/geotools/GeoPackageWriter.scala b/server/src/main/scala/ch/wsl/box/rest/io/geotools/GeoPackageWriter.scala
index 86fc78062..522c0f793 100644
--- a/server/src/main/scala/ch/wsl/box/rest/io/geotools/GeoPackageWriter.scala
+++ b/server/src/main/scala/ch/wsl/box/rest/io/geotools/GeoPackageWriter.scala
@@ -18,7 +18,7 @@ import scala.concurrent.{ExecutionContext, Future}
object GeoPackageWriter {
- def geomSchema(builder:SimpleFeatureTypeBuilder,name:String,geo:Seq[Option[GeoJson.Geometry]]) = {
+ def geomSchema(builder:SimpleFeatureTypeBuilder,name:String,geo:Seq[Option[GeoJson.Geometry]],srid:Int) = {
geo.toList.flatten.filterNot(_ == GeoJson.Empty).headOption match {
case Some(value) => {
value match {
@@ -33,7 +33,7 @@ object GeoPackageWriter {
case GeoJson.GeometryCollection(_, crs) => builder.add(name, classOf[Geometry], crs.srid)
}
}
- case None => builder.add(name, classOf[Geometry])
+ case None => builder.add(name, classOf[Geometry],srid)
}
}
@@ -60,6 +60,7 @@ object GeoPackageWriter {
}
+ def geomNameTrim(name:String) = if(name.trim.isEmpty) "geom" else name.trim
def write(name:String, data: DataResultTable)(implicit ex:ExecutionContext) = Future{
@@ -69,24 +70,56 @@ object GeoPackageWriter {
val geopkg = new GeoPackage(File.createTempFile("geopkg", "db"))
geopkg.init()
+ val geometry_by_type: Map[String, Seq[(GeoJson.Geometry,Map[String,Json])]] = data.types.filter(_.typ == JSONFieldTypes.GEOMETRY).flatMap { case geom =>
+ val values: Seq[(GeoJson.Geometry, Map[String, Json])] = data.toMap
+ .flatMap(r => r.get(geom.name)
+ .flatMap(_.as[GeoJson.Geometry].toOption
+ .map(x => (x,r))
+ )
+ )
+ val kinds = values.groupBy(_._1.geomName)
+ val r: Seq[(String, Seq[(GeoJson.Geometry, Map[String, Json])])] = kinds.map{ case (geomType, values) =>
+ val name = geomNameTrim(geom.name)
+ Seq(name,geomType.toLowerCase).mkString("_") -> values
+ }.toSeq
+ r
+ }.toMap
+
+ geometry_by_type.foreach { case (geomName, values) =>
+
+ val geomFieldName = data.types.find(c => c.typ == JSONFieldTypes.GEOMETRY && geomName.startsWith(geomNameTrim(c.name))).map(_.name).getOrElse("no-field")
+
+ val srid = values
+ .groupBy(_._1.crs.srid) // group the entrys by their SRID
+ .maxByOption(_._2.length) //take the srid with more entries
+ .map(_._1) // take the SRID
+ .getOrElse(0) //dafault SRID 0 -> no information
+ val builder: SimpleFeatureTypeBuilder = new SimpleFeatureTypeBuilder
+ builder.setName(s"${name}_$geomName")
+ //builder.setCRS(org.geotools.referencing.CRS.decode(s"EPSG:$srid"))
- data.geometry.foreach { case (geomName, values) =>
- val builder: SimpleFeatureTypeBuilder = new SimpleFeatureTypeBuilder
- builder.setName(s"${name}_$geomName")
- //builder.setCRS(org.geotools.referencing.CRS.decode(crs.name))
+ data.types.zipWithIndex.foreach { case (t,i) =>
+
+ // to avoid double name of columns group data by name and in case add suffix
+ val sameName = data.types.map(_.name.trim).zipWithIndex.filter(_._1 == t.name.trim)
+ val n = if(sameName.length == 1) t.name.trim else {
+ val suffix = sameName.indexWhere{case (_,j) => i == j} + 1
+ t.name.trim + "_" + suffix
+ }
+
+
+ t.typ match {
+ case JSONFieldTypes.INTEGER => builder.add(n, classOf[Integer])
+ case JSONFieldTypes.NUMBER => builder.add(n, classOf[Double])
+ case JSONFieldTypes.GEOMETRY if geomNameTrim(geomFieldName) == geomNameTrim(n) => geomSchema(builder, geomName, values.map(x => Some(x._1)), srid)
+ case JSONFieldTypes.GEOMETRY => ()
+ case _ => builder.add(n, classOf[String])
+ }
- data.types.foreach { t =>
- t.typ match {
- case JSONFieldTypes.INTEGER => builder.add(t.name, classOf[Integer])
- case JSONFieldTypes.NUMBER => builder.add(t.name, classOf[Double])
- case JSONFieldTypes.GEOMETRY if t.name == geomName => geomSchema(builder, t.name, values)
- case JSONFieldTypes.GEOMETRY => ()
- case _ => builder.add(t.name, classOf[String])
- }
}
val schema = builder.buildFeatureType()
@@ -95,8 +128,9 @@ object GeoPackageWriter {
val featureBuilder = new SimpleFeatureBuilder(schema)
- for (r <- data.toMap) yield {
- data.types.filter(tt => tt.typ != JSONFieldTypes.GEOMETRY || tt.name == geomName).foreach { c =>
+
+ for (r <- values.map(_._2)) yield {
+ data.types.filter(tt => tt.typ != JSONFieldTypes.GEOMETRY || geomName.startsWith(geomNameTrim(tt.name))).foreach { c =>
fieldWriter(r.getOrElse(c.name, Json.Null), c.typ, featureBuilder)
}
collection.add(featureBuilder.buildFeature(null))
@@ -111,8 +145,10 @@ object GeoPackageWriter {
val entry = new FeatureEntry()
- entry.setTableName(name)
+ entry.setTableName(name+"_"+geomName)
+ entry.setSrid(srid)
//entry.setDescription("Cities of the world")
+ geopkg.addCRS(srid)
geopkg.add(entry, collection)
//geopkg.createSpatialIndex(entry)
diff --git a/server/src/main/scala/ch/wsl/box/rest/logic/DbActions.scala b/server/src/main/scala/ch/wsl/box/rest/logic/DbActions.scala
index 3252cf184..74218204c 100755
--- a/server/src/main/scala/ch/wsl/box/rest/logic/DbActions.scala
+++ b/server/src/main/scala/ch/wsl/box/rest/logic/DbActions.scala
@@ -128,7 +128,10 @@ class DbActions[T <: ch.wsl.box.jdbc.PostgresProfile.api.Table[M] with UpdateTab
}
- def keys(): DBIOAction[Seq[String], NoStream, Effect] = DBIO.from(services.connection.adminDB.run(EntityMetadataFactory.keysOf(entity.baseTableRow.schemaName.getOrElse("public"),entity.baseTableRow.tableName)))
+ def keys(): DBIOAction[Seq[String], NoStream, Effect] = EntityMetadataFactory.cachedKeyOf(entity.baseTableRow.schemaName.getOrElse("public"),entity.baseTableRow.tableName) match {
+ case Some(k) => DBIO.successful(k)
+ case None => DBIO.from(services.connection.adminDB.run(EntityMetadataFactory.keysOf(entity.baseTableRow.schemaName.getOrElse("public"),entity.baseTableRow.tableName)))
+ }
override def ids(query: JSONQuery,keys:Seq[String]): DBIO[IDs] = {
diff --git a/server/src/main/scala/ch/wsl/box/rest/logic/FormActions.scala b/server/src/main/scala/ch/wsl/box/rest/logic/FormActions.scala
index 4b0d3d79f..dd663f69a 100755
--- a/server/src/main/scala/ch/wsl/box/rest/logic/FormActions.scala
+++ b/server/src/main/scala/ch/wsl/box/rest/logic/FormActions.scala
@@ -49,7 +49,7 @@ case class FormActions(metadata:JSONMetadata,
jsonAction.getById(id).flatMap{ row =>
- DBIO.sequenceOption(row.map(expandJson))
+ DBIO.sequenceOption(row.map(expandJson(_,metadata.fields.map(_.name))))
}
}
@@ -58,6 +58,7 @@ case class FormActions(metadata:JSONMetadata,
private def queryForm(query: JSONQuery):DBIO[JSONQuery] = {
val base = metadata.query.map{ defaultQuery =>
JSONQuery(
+ fields = query.fields,
filter = defaultQuery.filter ++ query.filter,
sort = query.sort ++ defaultQuery.sort,
paging = query.paging
@@ -67,17 +68,6 @@ case class FormActions(metadata:JSONMetadata,
}
- private def streamSeq(query:JSONQuery):DBIO[Seq[Json]] = {
-
- for{
- q <- queryForm(query)
- rows <- jsonAction.findSimple(q)
- result <- DBIO.sequence(rows.map(expandJson))
- } yield result
-
- }
-
-
override def distinctOn(fields: Seq[String], query: JSONQuery): DBIO[Seq[Json]] = jsonAction.distinctOn(fields, query)
override def findSimple(query:JSONQuery): DBIO[Seq[Json]] = {
@@ -85,7 +75,7 @@ case class FormActions(metadata:JSONMetadata,
for{
q <- queryForm(query)
rows <- jsonAction.findSimple(q)
- result <- DBIO.sequence(rows.map(expandJson))
+ result <- DBIO.sequence(rows.map(r => expandJson(r,query.fields.getOrElse(metadata.fields.map(_.name)))))
} yield result
}
@@ -104,7 +94,7 @@ case class FormActions(metadata:JSONMetadata,
private def _list(query:JSONQuery):DBIO[Seq[Json]] = {
queryForm(query).flatMap { q =>
metadata.view.map(v => Registry().actions(v)) match {
- case None => streamSeq(q)
+ case None => findSimple(q)
case Some(v) => v.findSimple(q)
}
}
@@ -173,7 +163,7 @@ case class FormActions(metadata:JSONMetadata,
}
}
- def list(query:JSONQuery,dropHtml:Boolean = false,fields:JSONMetadata => Seq[String] = _.tabularFields):DBIO[Seq[Json]] = __list(query, dropHtml).map{ rows =>
+ def list(query:JSONQuery,dropHtml:Boolean = false,fields:Seq[String]):DBIO[Seq[Json]] = __list(query.copy(fields=Some(fields)), dropHtml).map{ rows =>
rows.map(Json.fromFields)
}
@@ -414,9 +404,9 @@ case class FormActions(metadata:JSONMetadata,
FormActions(metadata,registry,metadataFactory,Some(field)).findSimple(query)
}
- private def expandJson(dataJson:Json):DBIO[Json] = {
+ private def expandJson(dataJson:Json,fields:Seq[String]):DBIO[Json] = {
- val values = metadata.fields.map{ field =>
+ val values = metadata.fields.filter(x => fields.contains(x.name)).map{ field =>
{(field.`type`,field.child) match {
case ("static",_) => DBIO.successful(field.name -> field.default.asJson) //set default value
case (_,None) => DBIO.successful(field.name -> dataJson.js(field.name)) //use given value
diff --git a/server/src/main/scala/ch/wsl/box/rest/logic/notification/DbNotify.scala b/server/src/main/scala/ch/wsl/box/rest/logic/notification/DbNotify.scala
new file mode 100644
index 000000000..eb7e01a7f
--- /dev/null
+++ b/server/src/main/scala/ch/wsl/box/rest/logic/notification/DbNotify.scala
@@ -0,0 +1,66 @@
+package ch.wsl.box.rest.logic.notification
+
+
+import ch.wsl.box.jdbc.Connection
+import scribe.Logging
+import cats.effect._
+import skunk._
+import skunk.implicits._
+import skunk.codec.all._
+import cats.effect.syntax.all._
+import natchez.Trace.Implicits.noop
+import skunk.data.Identifier
+import cats.effect.unsafe.implicits.global
+import ch.wsl.box.services.config.{Config, FullConfig}
+import io.circe.Json
+
+import scala.concurrent.{ExecutionContext, Future}
+
+
+trait DbNotify {
+ def listen(channel:String,callback: Json => Future[Boolean])
+}
+
+
+class DbNotifyHandlerImpl(connection:Connection,conf:FullConfig) extends DbNotify with Logging {
+
+ case class BoxChannel(channel:String,payload:Json)
+
+ implicit val dec = io.circe.generic.semiauto.deriveDecoder[BoxChannel]
+
+ def handleMessage(str:String):Option[BoxChannel] = io.circe.parser.parse(str).flatMap(_.as[BoxChannel]) match {
+ case Left(value) => {
+ logger.warn(s"Message $str not parsable")
+ None
+ }
+ case Right(value) => {
+ logger.debug(s"Recived message on channel ${value.channel}: ${value.payload}")
+ Some(value)
+ }
+ }
+
+ val callbacks = scala.collection.mutable.Map[String,(Json) => Future[Boolean]]()
+
+ def listen(channel:String,callback: Json => Future[Boolean]) = callbacks.put(channel,callback)
+
+ def listenAll() = {
+ logger.info(s"Setting up DB Listen")
+ val io = connection.notificationSession().use{ s =>
+ val channelName = s"box_${conf.boxSchemaName}_channel"
+ logger.info(s"Listening on $channelName")
+ val ch = s.channel(Identifier.fromString(channelName).toOption.get)
+ val lst = ch.listen(1024)
+ lst.evalMap { n =>
+ handleMessage(n.value).flatMap(m => callbacks.get(m.channel).map(c => (m.payload,c))) match {
+ case Some((m,callback)) => IO.fromFuture(IO(callback(m)))
+ case None => IO()
+ }
+ }.compile.drain
+
+ }
+ io.unsafeRunAndForget()
+ }
+
+ listenAll()
+
+}
diff --git a/server/src/main/scala/ch/wsl/box/rest/logic/notification/MailHandler.scala b/server/src/main/scala/ch/wsl/box/rest/logic/notification/MailHandler.scala
index e7b9fd92d..d91ad2050 100644
--- a/server/src/main/scala/ch/wsl/box/rest/logic/notification/MailHandler.scala
+++ b/server/src/main/scala/ch/wsl/box/rest/logic/notification/MailHandler.scala
@@ -1,31 +1,17 @@
package ch.wsl.box.rest.logic.notification
-import ch.wsl.box.services.Services
-import ch.wsl.box.services.mail.{Mail, MailService}
+
import ch.wsl.box.services.mail_dispatcher.MailDispatcherService
-import io.circe._
-import io.circe.syntax._
-import io.circe.parser._
-import io.circe.generic.auto._
import scribe.Logging
+import scala.concurrent.Future
-import scala.concurrent.{ExecutionContext, Future}
-
-class MailHandler(mailDispatcherService: MailDispatcherService) extends Logging {
-
- def listen()(implicit ex:ExecutionContext,services: Services):Unit = {
-
- def handleNotification(str:String): Future[Boolean] = {
- parse(str) match {
- case Left(err) => Future.failed(new Exception(err.message))
- case Right(_) => {
- mailDispatcherService.dispatchNow()
- Future.successful(true)
- }
- }
- }
+class MailHandler(dbNotify: DbNotify, mailDispatcherService: MailDispatcherService) extends Logging {
- NotificationsHandler.create("mail_feedback_channel",services.connection, handleNotification)
+ def listen():Unit = {
+ dbNotify.listen("mail_feedback_channel",_ => Future.successful{
+ mailDispatcherService.dispatchNow()
+ true
+ })
}
}
diff --git a/server/src/main/scala/ch/wsl/box/rest/logic/notification/NotificationsHandler.scala b/server/src/main/scala/ch/wsl/box/rest/logic/notification/NotificationsHandler.scala
deleted file mode 100644
index 75bb33186..000000000
--- a/server/src/main/scala/ch/wsl/box/rest/logic/notification/NotificationsHandler.scala
+++ /dev/null
@@ -1,110 +0,0 @@
-package ch.wsl.box.rest.logic.notification
-
-import java.util.Date
-
-import ch.wsl.box.jdbc.Connection
-import ch.wsl.box.services.Services
-import org.postgresql.PGConnection
-import scribe.Logging
-
-import scala.concurrent.{ExecutionContext, Future}
-import scala.util.{Failure, Success, Try}
-
-trait PgNotifier{
- def stop()
-}
-
-object NotificationsHandler {
-
- def create(channel:String,connection:Connection,callback: (String) => Future[Boolean])(implicit ec:ExecutionContext):PgNotifier = new PgNotifier {
- val listener = new Listener(connection,channel,callback)
- listener.start()
- override def stop(): Unit = listener.stopRunning()
- }
-
-}
-
-import java.sql.SQLException
-
-class Listener(connection:Connection,channel:String,callback: (String) => Future[Boolean])(implicit ec:ExecutionContext) extends Thread with Logging {
-
-
- val user = connection.adminUser
- var pgconn: PGConnection = null
- var conn:java.sql.Connection = null
-
- private var running = true
- def stopRunning() = {
- running = false
- }
-
- private def reloadConnection() = {
- conn = connection.dataSource(s"Notification $channel",connection.dbSchema).getConnection()
- val stmt = conn.createStatement
- val listenQuery = s"""SET ROLE "$user"; LISTEN $channel"""
- logger.info(listenQuery)
- stmt.execute(listenQuery)
- stmt.close
- pgconn = conn.asInstanceOf[PGConnection]
- }
-
- def select1() = {
- val stmt = conn.createStatement
- val rs = stmt.executeQuery(s"SELECT 1")
- rs.close()
- stmt.close()
- }
-
- reloadConnection()
-
-
- override def run(): Unit = {
- while ( running ) {
- Thread.sleep(500)
- try {
-
- // issue a dummy query to contact the backend
- // and receive any pending notifications.
- Try(select1()) match {
- case Success(value) => value
- case Failure(exception) => {
- Thread.sleep(1000)
- Try(conn.close())
- reloadConnection()
- select1()
- }
- }
-
-
- val notifications = pgconn.getNotifications(1000)
- if(notifications != null) {
- notifications.foreach{ n =>
- logger.info(s"""
- |Recived notification:
- |timestamp: ${new Date().toString}
- |name: ${n.getName}
- |parameter: ${n.getParameter}
- |""".stripMargin)
- callback(n.getParameter).onComplete {
- case Success(ok) => true
- case Failure(exception) => {
- exception.printStackTrace()
- logger.error(exception.getMessage)
- false
- }
- }
- }
- }
- // wait a while before checking again for new
- // notifications
- //Thread.sleep(1000)
- }
- catch {
- case sqle: SQLException =>
- sqle.printStackTrace()
- case ie: InterruptedException =>
- ie.printStackTrace()
- }
- }
- }
-}
\ No newline at end of file
diff --git a/server/src/main/scala/ch/wsl/box/rest/metadata/EntityMetadataFactory.scala b/server/src/main/scala/ch/wsl/box/rest/metadata/EntityMetadataFactory.scala
index 4684936d3..834de253a 100644
--- a/server/src/main/scala/ch/wsl/box/rest/metadata/EntityMetadataFactory.scala
+++ b/server/src/main/scala/ch/wsl/box/rest/metadata/EntityMetadataFactory.scala
@@ -184,11 +184,15 @@ object EntityMetadataFactory extends Logging {
}
}
- def keysOf(schema:String,table:String)(implicit ec:ExecutionContext,services:Services):DBIO[Seq[String]] = {
-
+ def cachedKeyOf(schema:String,table:String) = {
val cacheKey = (schema,table)
logger.info("Getting " + cacheKey + " keys")
- getSchema(schema).cacheKeys.get(table) match {
+ getSchema(schema).cacheKeys.get(table)
+ }
+
+ def keysOf(schema:String,table:String)(implicit ec:ExecutionContext,services:Services):DBIO[Seq[String]] = {
+
+ cachedKeyOf(schema,table) match {
case Some(r) => DBIO.successful(r)
case None => {
logger.info(s"Metadata keys cache miss! cache key: ($table)")
diff --git a/server/src/main/scala/ch/wsl/box/rest/metadata/InMemoryMetadataFactory.scala b/server/src/main/scala/ch/wsl/box/rest/metadata/InMemoryMetadataFactory.scala
new file mode 100644
index 000000000..54fa1f4c7
--- /dev/null
+++ b/server/src/main/scala/ch/wsl/box/rest/metadata/InMemoryMetadataFactory.scala
@@ -0,0 +1,52 @@
+//package ch.wsl.box.rest.metadata
+//
+//import ch.wsl.box.model.shared.{CurrentUser, JSONField, JSONMetadata}
+//import ch.wsl.box.services.Services
+//import slick.dbio.DBIO
+//
+//import java.util.UUID
+//import scala.collection.mutable.HashMap
+//import scala.concurrent.ExecutionContext
+//
+//class InMemoryMetadataFactory(mf:MetadataFactory) {
+//
+// private val metadataByName:HashMap[(String,String),JSONMetadata] = HashMap()
+// private val metadataByUUID:HashMap[(UUID,String),JSONMetadata] = HashMap()
+//
+// def load()(implicit ec: ExecutionContext, services: Services):DBIO[Boolean] = ???
+//
+// private def filterRoles(user:CurrentUser)(metadata:DBIO[JSONMetadata])(implicit ec:ExecutionContext):DBIO[JSONMetadata] = metadata.map{ m =>
+//
+// def filterRole(field:JSONField):Boolean = field.roles.isEmpty || field.roles.intersect(user.roles ++ Seq(user.username)).nonEmpty
+//
+// m.copy(fields = m.fields.filter(filterRole))
+// }
+//
+// def of(name: String, lang: String, user: CurrentUser)(implicit ec: ExecutionContext): DBIO[JSONMetadata] = filterRoles(user){
+// metadataByName.get((name, lang)) match {
+// case Some(value) => DBIO.successful(value)
+// case None => DBIO.failed(new Exception("Metadata not found"))
+// }
+// }
+//
+// def of(id: UUID, lang: String, user: CurrentUser)(implicit ec: ExecutionContext, services: Services): DBIO[JSONMetadata] = filterRoles(user){
+// metadataByUUID.get((id, lang)) match {
+// case Some(value) => DBIO.successful(value)
+// case None => DBIO.failed(new Exception("Metadata not found"))
+// }
+// }
+//
+// def children(form:JSONMetadata, user: CurrentUser,ignoreChilds:Seq[UUID] = Seq())(implicit ec:ExecutionContext,services:Services):DBIO[Seq[JSONMetadata]] = {
+// val childIds: Seq[UUID] = form.fields.flatMap(_.child).map(_.objId)
+//
+// for{
+// firstLevel <- DBIO.sequence{childIds.map{ objId => of(objId,form.lang,user)}}
+// secondLevel <- DBIO.sequence(firstLevel.map(y => children(y,user,ignoreChilds ++ childIds)))
+// } yield {
+// firstLevel ++ secondLevel.flatten
+// }
+//
+// }
+//
+// def list(implicit ec: ExecutionContext, services: Services): DBIO[Seq[String]] = ???
+//}
diff --git a/server/src/main/scala/ch/wsl/box/rest/routes/Exporters.scala b/server/src/main/scala/ch/wsl/box/rest/routes/Exporters.scala
index 72428d8b6..e5888564c 100644
--- a/server/src/main/scala/ch/wsl/box/rest/routes/Exporters.scala
+++ b/server/src/main/scala/ch/wsl/box/rest/routes/Exporters.scala
@@ -3,9 +3,9 @@ package ch.wsl.box.rest.routes
import akka.http.scaladsl.model.{HttpEntity, HttpResponse, MediaTypes}
import akka.http.scaladsl.model.headers.{ContentDispositionTypes, `Content-Disposition`}
import akka.http.scaladsl.server.Directives._
-import akka.http.scaladsl.server.Route
+import akka.http.scaladsl.server.{RequestContext, Route}
import akka.stream.Materializer
-import ch.wsl.box.model.shared.{CSVTable, JSONFieldLookupData, JSONFieldLookupExtractor, JSONFieldLookupRemote, JSONMetadata, JSONQuery, XLSTable}
+import ch.wsl.box.model.shared.{CSVTable, JSONField, JSONFieldLookupData, JSONFieldLookupExtractor, JSONFieldLookupRemote, JSONMetadata, JSONQuery, XLSTable}
import ch.wsl.box.rest.io.xls.XLS
import ch.wsl.box.rest.logic.{FormActions, Lookup}
import io.circe.parser.parse
@@ -20,7 +20,7 @@ import ch.wsl.box.services.Services
import ch.wsl.box.shared.utils.JSONUtils.EnhancedJson
import io.circe.Json
-import scala.concurrent.ExecutionContext
+import scala.concurrent.{ExecutionContext, Future}
trait Exporters {
@@ -37,7 +37,7 @@ trait Exporters {
val registry: RegistryInstance
val name:String
val metadataFactory: MetadataFactory
- def tabularMetadata(fields:Option[Seq[String]] = None): DBIO[JSONMetadata]
+ def tabularMetadata(): DBIO[JSONMetadata]
def actions:FormActions
def mergeWithForeignKeys(extractFk: Boolean,data:Seq[Json],fk: Map[String,Seq[Json]],metadata:JSONMetadata):Seq[Json] = {
@@ -74,7 +74,7 @@ trait Exporters {
metadata <- DBIO.from(boxDb.adminDb.run(tabularMetadata()))
formActions = FormActions(metadata, registry, metadataFactory)
fkValues <- Lookup.valuesForEntity(metadata)
- data <- formActions.list(query, true, _.exportFieldsNoGeom.map(_.name))
+ data <- formActions.list(query, true, metadata.exportFieldsNoGeom.map(_.name))
xlsTable = XLSTable(
title = name,
header = metadata.exportFieldsNoGeom.map(_.title),
@@ -88,24 +88,38 @@ trait Exporters {
}
}
- def exportCsv(q:String,fk:Option[String])(implicit session:BoxSession, db:FullDatabase, mat:Materializer, ec:ExecutionContext, services:Services) = {
+ def exportCsv(q:String,fk:Option[String],_fields:Option[String])(implicit session:BoxSession, db:FullDatabase, mat:Materializer, ec:ExecutionContext, services:Services): Route = {
val extractFk = fk.forall(_ == "resolve_fk")
val query = parse(q).right.get.as[JSONQuery].right.get
- val io = for {
- metadata <- DBIO.from(boxDb.adminDb.run(tabularMetadata()))
- formActions = FormActions(metadata, registry, metadataFactory)
- fkValues <- Lookup.valuesForEntity(metadata)
- data <- formActions.list(query, true, _.exportFieldsNoGeom.map(_.name))
- csvTable = CSVTable(
- title = name,
- header = metadata.exportFieldsNoGeom.map(_.title),
- rows = mergeWithForeignKeys(extractFk,data,fkValues,metadata).map(row => metadata.exportFieldsNoGeom.map(cell => row.get(cell.name)))
- )
- } yield {
- CSV.download(csvTable)
+
+
+ def selectedFields(mf:Seq[JSONField]):Seq[JSONField] = {
+ _fields.map(_.split(",").toSeq) match {
+ case Some(value) => value.flatMap(f => mf.find(_.name == f))
+ case None => mf
+ }
}
- onSuccess(db.db.run(io))(x => x)
+
+ val fut: Future[Route] = boxDb.adminDb.run(tabularMetadata()).flatMap { metadata =>
+
+ val formActions = FormActions(metadata, registry, metadataFactory)
+ val fields = selectedFields(metadata.exportFieldsNoGeom)
+ val io = for {
+ fkValues <- Lookup.valuesForEntity(metadata)
+ data <- formActions.list(query, true, fields.map(_.name))
+ csvTable = CSVTable(
+ title = name,
+ header = fields.map(_.title),
+ rows = mergeWithForeignKeys(extractFk, data, fkValues, metadata).map(row => fields.map(cell => row.get(cell.name)))
+ )
+ } yield {
+ CSV.download(csvTable)
+ }
+ db.db.run(io)
+ }
+
+ rc:RequestContext => fut.flatMap(x => x(rc))
}
// def shp(implicit session:BoxSession, db:FullDatabase, mat:Materializer, ec:ExecutionContext, services:Services): Route = path("shp") {
diff --git a/server/src/main/scala/ch/wsl/box/rest/routes/File.scala b/server/src/main/scala/ch/wsl/box/rest/routes/File.scala
index 8917bd865..6b91056b4 100755
--- a/server/src/main/scala/ch/wsl/box/rest/routes/File.scala
+++ b/server/src/main/scala/ch/wsl/box/rest/routes/File.scala
@@ -24,6 +24,7 @@ import ch.wsl.box.rest.runtime.Registry
import ch.wsl.box.rest.utils.UserProfile
import ch.wsl.box.services.Services
import ch.wsl.box.services.file.FileId
+import org.apache.tika.Tika
import scala.concurrent.duration.DurationInt
import scala.concurrent.{Await, ExecutionContext, Future}
@@ -41,10 +42,15 @@ object File{
def completeFile(f:BoxFile) =
complete {
+ val file = f.file.getOrElse(Array())
val contentType = f.mime.flatMap{ mime =>
ContentType.parse(mime).right.toOption
+ }.orElse{
+ val tika = new Tika()
+ val mime = tika.detect(file.take(4096))
+ ContentType.parse(mime).right.toOption
}.getOrElse(ContentTypes.`application/octet-stream`)
- val file = f.file.getOrElse(Array())
+
val name = f.name
val entity = HttpEntity(contentType,file)
diff --git a/server/src/main/scala/ch/wsl/box/rest/routes/Form.scala b/server/src/main/scala/ch/wsl/box/rest/routes/Form.scala
index fe61a4fb1..6cb4e2a80 100755
--- a/server/src/main/scala/ch/wsl/box/rest/routes/Form.scala
+++ b/server/src/main/scala/ch/wsl/box/rest/routes/Form.scala
@@ -60,32 +60,22 @@ case class Form(
def metadata: JSONMetadata = Await.result(boxDb.adminDb.run(metadataFactory.of(name,lang,session.user)),10.seconds)
def actions:FormActions = FormActions(metadata,registry,metadataFactory)
- private def _tabMetadata(fields:Option[Seq[String]] = None,m:JSONMetadata): Seq[JSONField] = {
- fields match {
- case Some(fields) => m.fields.filter(field => fields.contains(field.name))
- case None => m.fields.filter(field => m.tabularFields.contains(field.name) || m.exportFields.contains(field.name))
- }
- }
- private def viewTableMetadata(fields:Seq[String],tableMetadata:JSONMetadata,viewMetadata:JSONMetadata): Seq[JSONField] = {
- val tableFields = _tabMetadata(Some(fields),tableMetadata)
- val viewFields = _tabMetadata(Some(fields),viewMetadata)
- fields.flatMap{ field =>
- tableFields.find(_.name == field).orElse(viewFields.find(_.name == field))
- }
+ private def viewTableMetadata(formMetadata:JSONMetadata,viewMetadata:JSONMetadata): JSONMetadata = {
+
+ val allFields = formMetadata.fields ++ viewMetadata.fields.filter(f => !formMetadata.fields.map(_.name).contains(f.name))
+ formMetadata.copy(fields = allFields)
}
- def tabularMetadata(fields:Option[Seq[String]] = None): DBIO[JSONMetadata] = {
- val filteredFields = metadata.view match {
- case None => DBIO.successful(_tabMetadata(fields,metadata))
+ def tabularMetadata(): DBIO[JSONMetadata] = {
+ metadata.view match {
+ case None => DBIO.successful(metadata)
case Some(view) => DBIO.from(EntityMetadataFactory.of(view,registry).map{ vm =>
- viewTableMetadata(fields.getOrElse(metadata.tabularFields),metadata,vm)
+ viewTableMetadata(metadata,vm)
})
}
- filteredFields.map( ff => metadata.copy(fields = ff ))
-
}
def privateOnly(r: => Route):Route = {
@@ -116,7 +106,7 @@ case class Form(
} ~ get {
privateOnly {
parameters('q, 'fk.?, 'fields.?) { (q, fk, fields) =>
- exportCsv(q,fk)
+ exportCsv(q,fk,fields)
}
}
}
@@ -268,7 +258,7 @@ case class Form(
val io = for {
metadata <- DBIO.from(boxDb.adminDb.run(tabularMetadata()))
formActions = FormActions(metadata, registry, metadataFactory)
- result <- formActions.list(query, true)
+ result <- formActions.list(query, true, metadata.tabularFields)
} yield {
result
}
diff --git a/server/src/main/scala/ch/wsl/box/rest/routes/Table.scala b/server/src/main/scala/ch/wsl/box/rest/routes/Table.scala
index 405041016..13480c427 100755
--- a/server/src/main/scala/ch/wsl/box/rest/routes/Table.scala
+++ b/server/src/main/scala/ch/wsl/box/rest/routes/Table.scala
@@ -218,15 +218,19 @@ case class Table[T <: ch.wsl.box.jdbc.PostgresProfile.api.Table[M] with UpdateTa
} ~
respondWithHeader(`Content-Disposition`(ContentDispositionTypes.attachment,Map("filename" -> s"$name.csv"))) {
get {
- parameters('q) { q =>
+ parameters('q,'fields.?) { (q,fields) =>
import kantan.csv._
import kantan.csv.ops._
val query = parse(q).right.get.as[JSONQuery].right.get
val csvString = for{
metadata <- EntityMetadataFactory.of(name, registry)
- data <- db.run(dbActions.findSimple(query))
- } yield (Seq(metadata.fields.map(_.name)) ++ data.map(_.values())).asCsv(rfc)
+ selectedFields = fields.map(_.split(",").toSeq) match {
+ case Some(value) => value.flatMap(f => metadata.fields.find(_.name == f.name)).map(_.name)
+ case None => metadata.fields.map(_.name)
+ }
+ data <- db.run(dbActions.fetchFields(selectedFields,query))
+ } yield (Seq(selectedFields) ++ data.map(r => selectedFields.map(f => r.get(f)))).asCsv(rfc)
onComplete(csvString) {
case Success(csv) => complete(csv)
diff --git a/server/src/main/scala/ch/wsl/box/rest/routes/UI.scala b/server/src/main/scala/ch/wsl/box/rest/routes/UI.scala
index 1ab97485a..c1b38ccb2 100755
--- a/server/src/main/scala/ch/wsl/box/rest/routes/UI.scala
+++ b/server/src/main/scala/ch/wsl/box/rest/routes/UI.scala
@@ -24,72 +24,19 @@ object UI {
import Directives._
- val postgresWorker = path("postgres.worker.js") {
- get{
- val worker = s"""
- |try {
- | import { PGlite } from '@electric-sql/pglite'
- |} catch (error) {
- | console.error('Error importing PGlite:', error);
- |}
- |
- |try {
- | import { worker } from '@electric-sql/pglite/worker'
- |} catch (error) {
- | console.error('Error importing worker:', error);
- |}
- |
- |try{
- | worker({
- | async init() {
- | // Create and return a PGlite instance
- | return new PGlite('idb://box-pgdata')
- | },
- | })
- |} catch (error) {
- | console.error('Error starting worker:', error);
- |}
- |""".stripMargin
-
- val simpleWorker =
- s"""
- |console.log('Worker started');
- |
- |try {
- | const { PGlite } = await import('./assets/@electric-sql/pglite/dist/index.js')
- | const { worker } = await import('./assets/@electric-sql/pglite/dist/worker/index.js')
- | worker({
- | async init() {
- | // Create and return a PGlite instance
- | return new PGlite('idb://box-pgdata')
- | },
- | })
- |} catch (error) {
- | console.error('Error starting worker:', error);
- |}
- |
- |self.onmessage = function(event) {
- | console.log('Message received in worker:', event.data);
- |};
- |""".stripMargin
+ def resourcesAt(basePath:String) = pathPrefix(basePath) {
+ extractUnmatchedPath { path =>
+ val assetName = path.toString().substring(1)
+ path.endsWith("wasm") match {
+ case false => getFromResource(s"$basePath/$assetName")
+ case true => getFromResource(s"$basePath/$assetName", contentType = ContentType.apply(MediaType.applicationBinary("wasm",MediaType.Compressible)))
+ }
- complete(HttpEntity(MediaTypes.`application/javascript`.toContentType(HttpCharsets.`UTF-8`) ,simpleWorker))
}
}
- val postgresWasm = path("postgres.wasm") {
- WebJarsSupport.fullPath("@electric-sql/pglite/dist/postgres.wasm",ContentType.apply(MediaType.applicationBinary("wasm",MediaType.Compressible)))
- }
- val postgresData = path("postgres.data") {
- WebJarsSupport.fullPath("@electric-sql/pglite/dist/postgres.data",ContentTypes.`text/plain(UTF-8)`)
- }
-
def clientFiles(implicit system:ActorSystem,services:Services):Route = {
- pathPrefix("dev") {
- getFromBrowseableDirectories("./client/target/scala-2.13/scalajs-bundler/main")
- } ~
- postgresWorker ~
pathPrefix("icon") {
pathPrefix("icon.png") {
get{
@@ -112,9 +59,6 @@ object UI {
File.completeFile(BoxFile(Some(IconGenerator.withName(services.config.initials,services.config.mainColor,16,8)),Some("image/png"),"favicon-16x16.png"))
}
} ~
- pathPrefix("sw.js") {
- getFromResource("sw.js")
- } ~
pathPrefix("pdf") {
extractUnmatchedPath { e =>
@@ -152,28 +96,12 @@ object UI {
complete(HttpEntity(ContentType(MediaType.applicationWithOpenCharset("manifest+json","webmanifest"),HttpCharsets.`UTF-8`) ,manifest))
}
} ~
- pathPrefix("assets") {
- postgresWasm ~
- postgresData ~
- WebJarsSupport.webJars
- } ~
- pathPrefix("bundle") {
- WebJarsSupport.bundle
- } ~
- pathPrefix("redactor.js") {
- get{
- complete(HttpEntity(ContentType(MediaTypes.`application/javascript`,HttpCharsets.`UTF-8`) ,services.config.redactorJs))
- }
- }~
- pathPrefix("redactor.css") {
- get{
- complete(HttpEntity(ContentType(MediaTypes.`text/css`,HttpCharsets.`UTF-8`) ,services.config.redactorCSS))
- }
- } ~
+ resourcesAt("ui") ~
+ resourcesAt("public") ~
get {
complete {
val module = if(services.config.localDb) AvailableUIModule.prod else AvailableUIModule.prodNoLocalDb
- ch.wsl.box.templates.html.index.render(BoxBuildInfo.version,module,services.config.enableRedactor,services.config.devServer,services.config.basePath,services.config.mainColor,services.config.matomo)
+ ch.wsl.box.templates.html.index.render(BoxBuildInfo.version,module,services.config.frontendUrl,services.config.mainColor,services.config.matomo)
}
}
}
diff --git a/server/src/main/scala/ch/wsl/box/rest/routes/v1/Admin.scala b/server/src/main/scala/ch/wsl/box/rest/routes/v1/Admin.scala
index f499569a7..eb99366f1 100644
--- a/server/src/main/scala/ch/wsl/box/rest/routes/v1/Admin.scala
+++ b/server/src/main/scala/ch/wsl/box/rest/routes/v1/Admin.scala
@@ -7,7 +7,7 @@ import akka.http.scaladsl.server.Directives.{complete, get, path, pathPrefix}
import akka.stream.Materializer
import ch.wsl.box.information_schema.PgInformationSchema
import ch.wsl.box.model.shared.admin.FormCreationRequest
-import ch.wsl.box.model.{BoxDefinition, BoxDefinitionMerge, InformationSchema}
+import ch.wsl.box.model.{BoxDefinition, BoxDefinitionMerge, InformationSchema, Translations}
import ch.wsl.box.model.shared.{BoxTranslationsFields, EntityKind}
import ch.wsl.box.rest.metadata.{BoxFormMetadataFactory, FormCreationHandler, StubMetadataFactory}
import ch.wsl.box.rest.routes.{Form, Table}
@@ -19,8 +19,9 @@ import io.circe._
import io.circe.generic.auto._
import io.circe.syntax._
import ch.wsl.box.jdbc.PostgresProfile.api._
+import ch.wsl.box.rest.logic.LangHelper
-import scala.concurrent.ExecutionContext
+import scala.concurrent.{ExecutionContext, Future}
case class Admin(session:BoxSession)(implicit ec:ExecutionContext, userProfile: UserProfile, mat:Materializer, system:ActorSystem, services: Services) {
@@ -117,6 +118,45 @@ case class Admin(session:BoxSession)(implicit ec:ExecutionContext, userProfile:
}
+ def translate =pathPrefix("translate") {
+ pathPrefix(Segment) { from =>
+ path(Segment) { to =>
+ post {
+ entity(as[String]) { body =>
+ complete {
+ services.translation.translate(from, to, body)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ def translateAll = pathPrefix("translate-all") {
+ pathPrefix(Segment) { from =>
+ pathPrefix(Segment) { to =>
+ get{
+ complete{
+ Translations.autoTranslate(from,to)
+ }
+ }
+ }
+ }
+ }
+
+ def translateAllForce = pathPrefix("translate-all-force") {
+ pathPrefix(Segment) { from =>
+ pathPrefix(Segment) { to =>
+ get{
+ complete{
+ Translations.autoTranslate(from,to,true)
+ }
+ }
+ }
+ }
+ }
+
+
val route = Auth.onlyAdminstrator(session) { //need to be at the end or non administrator request are not resolved
//access to box tables for administrator
form(session) ~
@@ -128,6 +168,9 @@ case class Admin(session:BoxSession)(implicit ec:ExecutionContext, userProfile:
boxDefinition ~
childCandidates ~
roles ~
- createForm
+ createForm ~
+ translate ~
+ translateAll ~
+ translateAllForce
}
}
diff --git a/server/src/main/scala/ch/wsl/box/rest/routes/v1/WebsocketNotifications.scala b/server/src/main/scala/ch/wsl/box/rest/routes/v1/WebsocketNotifications.scala
index 6fe0cf054..0e3406b6b 100644
--- a/server/src/main/scala/ch/wsl/box/rest/routes/v1/WebsocketNotifications.scala
+++ b/server/src/main/scala/ch/wsl/box/rest/routes/v1/WebsocketNotifications.scala
@@ -6,7 +6,7 @@ import akka.http.scaladsl.server.Directives
import akka.stream.{CompletionStrategy, Materializer, OverflowStrategy}
import akka.stream.scaladsl.{Flow, Sink, Source}
import ch.wsl.box.jdbc.Connection
-import ch.wsl.box.rest.logic.notification.NotificationsHandler
+import ch.wsl.box.rest.logic.notification.{DbNotify}
import ch.wsl.box.rest.utils.UserProfile
import ch.wsl.box.services.Services
import io.circe._
@@ -62,7 +62,7 @@ trait NotificationChannels {
def start()
}
-class NotificationChannelsImpl(connection:Connection) extends NotificationChannels with Logging {
+class NotificationChannelsImpl(connection:Connection,dbNotify: DbNotify) extends NotificationChannels with Logging {
private var notificationChannels: ListBuffer[NotificationChannel] = ListBuffer.empty[NotificationChannel]
def add(user:String,topic: String)(implicit mat: Materializer) = {
@@ -74,19 +74,16 @@ class NotificationChannelsImpl(connection:Connection) extends NotificationChanne
final val ALL_USERS = "ALL_USERS"
- private def handleNotification(str:String): Future[Boolean] = {
- parse(str) match {
- case Left(err) => Future.failed(new Exception(err.message))
- case Right(js) => js.as[UiNotification] match {
- case Left(err) => Future.failed(new Exception(err.message + err.history))
- case Right(notification) => {
- logger.info(s"Send notification: $notification")
- notification.allowed_users.contains(ALL_USERS) match {
- case true => notificationChannels.foreach(_.sendBroadcast(notification))
- case false => notificationChannels.foreach(_.sendNotification(notification))
- }
- Future.successful(true)
+ private def handleNotification(js:Json): Future[Boolean] = {
+ js.as[UiNotification] match {
+ case Left(err) => Future.failed(new Exception(err.message + err.history))
+ case Right(notification) => {
+ logger.info(s"Send notification: $notification")
+ notification.allowed_users.contains(ALL_USERS) match {
+ case true => notificationChannels.foreach(_.sendBroadcast(notification))
+ case false => notificationChannels.foreach(_.sendNotification(notification))
}
+ Future.successful(true)
}
}
}
@@ -95,5 +92,5 @@ class NotificationChannelsImpl(connection:Connection) extends NotificationChanne
- override def start(): Unit = NotificationsHandler.create("ui_feedback_channel",connection,handleNotification)
+ override def start(): Unit = dbNotify.listen("ui_feedback_channel",handleNotification)
}
diff --git a/server/src/main/scala/ch/wsl/box/services/Services.scala b/server/src/main/scala/ch/wsl/box/services/Services.scala
index b6ce9c344..5a0605ae3 100644
--- a/server/src/main/scala/ch/wsl/box/services/Services.scala
+++ b/server/src/main/scala/ch/wsl/box/services/Services.scala
@@ -2,12 +2,14 @@ package ch.wsl.box.services
import akka.actor.ActorSystem
import ch.wsl.box.jdbc.Connection
+import ch.wsl.box.rest.logic.notification.{DbNotify}
import ch.wsl.box.rest.routes.v1.NotificationChannels
import ch.wsl.box.rest.utils.BoxSession
import ch.wsl.box.services.config.{Config, FullConfig}
import ch.wsl.box.services.files.ImageCache
import ch.wsl.box.services.mail.MailService
import ch.wsl.box.services.mail_dispatcher.MailDispatcherService
+import ch.wsl.box.services.translation.TranslateService
import com.softwaremill.session.RefreshTokenStorage
import wvlet.airframe._
@@ -17,7 +19,10 @@ trait Services extends ServicesWithoutGeneration {
val actorSystem = bind[ActorSystem]
val imageCacher = bind[ImageCache]
val mail = bind[MailService]
+ val dbNotify = bind[DbNotify]
val mailDispatcher = bind[MailDispatcherService]
val notificationChannels = bind[NotificationChannels]
val refreshTokenStorage = bind[RefreshTokenStorage[BoxSession]]
+ val translation = bind[TranslateService]
+
}
diff --git a/server/src/main/scala/ch/wsl/box/services/config/ConfFileAndDb.scala b/server/src/main/scala/ch/wsl/box/services/config/ConfFileAndDb.scala
index b91497e1d..d26931997 100644
--- a/server/src/main/scala/ch/wsl/box/services/config/ConfFileAndDb.scala
+++ b/server/src/main/scala/ch/wsl/box/services/config/ConfFileAndDb.scala
@@ -20,7 +20,7 @@ import io.circe.syntax._
import io.circe.generic.auto._
class ConfFileAndDb(connection:Connection)(implicit ec:ExecutionContext) extends ConfigFileImpl with FullConfig with Logging {
- private var _conf: Map[String, String] = Map()
+ var _conf: Map[String, String] = Map()
def load() = {
@@ -57,7 +57,8 @@ class ConfFileAndDb(connection:Connection)(implicit ec:ExecutionContext) extends
"fks.lookup.labels",
"fks.lookup.rowsLimit",
"redactor.js",
- "redactor.css"
+ "redactor.css",
+ "deeplKey"
).contains(k)}
@@ -109,13 +110,6 @@ class ConfFileAndDb(connection:Connection)(implicit ec:ExecutionContext) extends
result
}
- def enableRedactor:Boolean = {
- Try(_conf("redactor.js")).toOption.exists(_.nonEmpty) &&
- Try(_conf("redactor.css")).toOption.exists(_.nonEmpty)
- }
-
- def redactorJs = Try(_conf("redactor.js")).getOrElse("")
- def redactorCSS = Try(_conf("redactor.css")).getOrElse("")
def filterPrecisionDatetime = Try(_conf("filter.precision.datetime").toUpperCase).toOption match {
case Some("DATE") => JSONFieldTypes.DATE
@@ -148,4 +142,9 @@ class ConfFileAndDb(connection:Connection)(implicit ec:ExecutionContext) extends
override def localDb: Boolean = Try(_conf("local.db").toBoolean).getOrElse(true)
override def singleUser: Boolean = conf.as[Option[Boolean]]("singleUser").getOrElse(false)
+
+ override def deeplKey: Option[String] = _conf.get("deeplKey")
+
+ override def deeplEndpoint: Option[String] = _conf.get("deeplEndpoint")
+
}
diff --git a/server/src/main/scala/ch/wsl/box/services/config/DummyFullConfig.scala b/server/src/main/scala/ch/wsl/box/services/config/DummyFullConfig.scala
index 24c9a42da..8b1c03353 100644
--- a/server/src/main/scala/ch/wsl/box/services/config/DummyFullConfig.scala
+++ b/server/src/main/scala/ch/wsl/box/services/config/DummyFullConfig.scala
@@ -34,12 +34,6 @@ class DummyFullConfig extends DummyConfigImpl with FullConfig {
override def fksLookupRowsLimit: Int = 50
- override def enableRedactor: Boolean = false
-
- override def redactorJs: String = ""
-
- override def redactorCSS: String = ""
-
override def devServer: Boolean = false
override def clientConf: Map[String, String] = Map()
diff --git a/server/src/main/scala/ch/wsl/box/services/config/FullConfig.scala b/server/src/main/scala/ch/wsl/box/services/config/FullConfig.scala
index eb8fd3a45..38f619945 100644
--- a/server/src/main/scala/ch/wsl/box/services/config/FullConfig.scala
+++ b/server/src/main/scala/ch/wsl/box/services/config/FullConfig.scala
@@ -5,7 +5,12 @@ import ch.wsl.box.viewmodel.MatomoConfig
import java.time.{LocalDateTime, OffsetDateTime}
-trait FullConfig extends Config {
+trait DeepLConfig {
+ def deeplKey:Option[String] = None
+ def deeplEndpoint:Option[String] = None
+}
+
+trait FullConfig extends Config with DeepLConfig {
def akkaHttpSession:com.typesafe.config.Config
def host:String
def port:Int
@@ -18,9 +23,6 @@ trait FullConfig extends Config {
def enableCache:Boolean
def fksLookupLabels:com.typesafe.config.Config
def fksLookupRowsLimit:Int
- def enableRedactor:Boolean
- def redactorJs:String
- def redactorCSS:String
def devServer:Boolean
def frontendUrl:String
diff --git a/server/src/main/scala/ch/wsl/box/services/config/FullConfigFileOnlyImpl.scala b/server/src/main/scala/ch/wsl/box/services/config/FullConfigFileOnlyImpl.scala
index 010fcb177..0acf8ae47 100644
--- a/server/src/main/scala/ch/wsl/box/services/config/FullConfigFileOnlyImpl.scala
+++ b/server/src/main/scala/ch/wsl/box/services/config/FullConfigFileOnlyImpl.scala
@@ -34,12 +34,6 @@ class FullConfigFileOnlyImpl extends ConfigFileImpl with FullConfig {
override def fksLookupRowsLimit: Int = throw new ConfigNotAvailableException
- override def enableRedactor: Boolean = throw new ConfigNotAvailableException
-
- override def redactorJs: String = throw new ConfigNotAvailableException
-
- override def redactorCSS: String = throw new ConfigNotAvailableException
-
override def devServer: Boolean = throw new ConfigNotAvailableException
override def clientConf: Map[String, String] = throw new ConfigNotAvailableException
diff --git a/server/src/main/scala/ch/wsl/box/services/translation/ColumnTranslate.scala b/server/src/main/scala/ch/wsl/box/services/translation/ColumnTranslate.scala
new file mode 100644
index 000000000..df3a29c41
--- /dev/null
+++ b/server/src/main/scala/ch/wsl/box/services/translation/ColumnTranslate.scala
@@ -0,0 +1,57 @@
+package ch.wsl.box.services.translation
+
+import ch.wsl.box.jdbc.{Connection, FullDatabase}
+import ch.wsl.box.model.shared.{JSONDiff, JSONDiffField, JSONDiffModel, JSONID, JSONQuery}
+import ch.wsl.box.rest.logic.notification.DbNotify
+import ch.wsl.box.rest.metadata.EntityMetadataFactory
+import ch.wsl.box.rest.runtime.Registry
+import ch.wsl.box.rest.utils.UserProfile
+import ch.wsl.box.services.Services
+import ch.wsl.box.shared.utils.JSONUtils.EnhancedJson
+import io.circe.Json
+import scribe.{Logger, Logging}
+import slick.dbio.DBIO
+
+import scala.concurrent.{ExecutionContext, Future}
+
+case class ColumnTranslateRequest(schema:String,table:String,from_lang:String,to_lang:String,from_column:String,to_column:String)
+
+class ColumnTranslate(dbNotify: DbNotify,translateService: TranslateService, connection: Connection)(implicit ec:ExecutionContext,services:Services) extends Logging {
+
+ implicit val dec = io.circe.generic.semiauto.deriveDecoder[ColumnTranslateRequest]
+
+ def translate(tr:ColumnTranslateRequest) = {
+
+ val actions = Registry().actions(tr.table)
+
+ implicit val fd = FullDatabase(connection.adminDB,connection.adminDB)
+ implicit val up = UserProfile(connection.adminUser,connection.adminUser)
+
+ for{
+ metadata <- EntityMetadataFactory.of(tr.table,Registry())
+ from <- connection.adminDB.run{
+ actions.fetchFields(metadata.keys ++ Seq(tr.from_column,tr.to_column),JSONQuery.limit(100000))
+ }
+ translated <- translateService.translateAll(tr.from_lang,tr.to_lang,from.map(_.get(tr.from_column)))
+ _ <- connection.adminDB.run {
+ DBIO.sequence(from.zip(translated).map { case (row, translated) =>
+ val diff = JSONDiff(Seq(
+ JSONDiffModel(tr.table, JSONID.fromData(row, metadata), Seq(JSONDiffField(tr.to_column, row.jsOpt(tr.to_column), Some(Json.fromString(translated)))))
+ ))
+ actions.updateDiff(diff)
+ })
+ }
+ } yield true
+
+ }
+
+ dbNotify.listen("translate_column", js => {
+ js.as[ColumnTranslateRequest] match {
+ case Left(value) => {
+ logger.warn(s"Channel translate_column, can't decode $js")
+ Future.successful(false)
+ }
+ case Right(value) => translate(value)
+ }
+ })
+}
diff --git a/server/src/main/scala/ch/wsl/box/services/translation/DeepLTranslateService.scala b/server/src/main/scala/ch/wsl/box/services/translation/DeepLTranslateService.scala
new file mode 100644
index 000000000..2acde2511
--- /dev/null
+++ b/server/src/main/scala/ch/wsl/box/services/translation/DeepLTranslateService.scala
@@ -0,0 +1,82 @@
+package ch.wsl.box.services.translation
+
+import ch.wsl.box.rest.auth.oidc.AuthFlow.OpenIDToken
+import ch.wsl.box.services.config.{DeepLConfig, FullConfig}
+import sttp.client4._
+import sttp.client4.circe.asJson
+import sttp.client4.{DefaultFutureBackend, basicRequest}
+import io.circe.generic.semiauto._
+
+import scala.concurrent.{ExecutionContext, Future}
+
+
+
+class DeepLTranslateService(conf: DeepLConfig)(implicit ex:ExecutionContext) extends TranslateService {
+
+ private val apiKey = conf.deeplKey
+ private val apiEndpoint = conf.deeplEndpoint
+
+ private val backend = DefaultFutureBackend()
+
+
+ private case class DeepLAPIRequest(
+ text:Seq[String],
+ target_lang: String,
+ source_lang: String,
+ context:Option[String]
+ )
+
+
+
+ private case class DeepLAPITranslations(
+ text:String
+ )
+
+ private case class DeepLAPIResponse(
+ translations: Seq[DeepLAPITranslations]
+ )
+
+ implicit private val reqEncoder = deriveEncoder[DeepLAPIRequest]
+ implicit private val resTDecoder = deriveDecoder[DeepLAPITranslations]
+ implicit private val resDecoder = deriveDecoder[DeepLAPIResponse]
+
+ override def translate(from: String, to: String,text: String): Future[String] = translateAll(from,to,Seq(text)).map(_.head)
+
+ override def translateAll(from: String, to: String,texts: Seq[String]): Future[Seq[String]] = {
+
+ val translator = translatorBulk(from,to) _
+
+ val blocks = texts.grouped(50)
+
+ blocks.foldLeft(Future.successful(Seq[String]())) { (accF, text) =>
+ for {
+ acc <- accF
+ res <- translator(text)
+ throttle <- Future{ Thread.sleep(300) }
+ } yield acc ++ res
+ }
+ }
+
+ private def translatorBulk(from: String, to: String)(texts: Seq[String]): Future[Seq[String]] = {
+ (apiKey,apiEndpoint) match {
+ case (Some(key),Some(endpoint)) => {
+ val r = basicRequest
+ .post(uri"${endpoint}/v2/translate")
+ .header("Authorization",s"DeepL-Auth-Key $key")
+ .body(asJson(DeepLAPIRequest(texts,to.toUpperCase,from.toUpperCase,Some("Text is used in a web application that expose database tables on the web."))))
+ .response(asJson[DeepLAPIResponse])
+
+ print(r.toCurl)
+
+ r.send(backend).map(_.body match {
+ case Right(value) => value.translations.map(_.text)
+ case Left(value) => throw value
+ })
+ }
+ case (None,Some(_)) => throw new Exception("DeepL API Key not found")
+ case (Some(_),None) => throw new Exception("DeepL Endpoint not found")
+ case (None,None) => throw new Exception("DeepL API Key and endpoint not found")
+ }
+ }
+
+}
diff --git a/server/src/main/scala/ch/wsl/box/services/translation/TranslateService.scala b/server/src/main/scala/ch/wsl/box/services/translation/TranslateService.scala
new file mode 100644
index 000000000..f182eac81
--- /dev/null
+++ b/server/src/main/scala/ch/wsl/box/services/translation/TranslateService.scala
@@ -0,0 +1,8 @@
+package ch.wsl.box.services.translation
+
+import scala.concurrent.Future
+
+trait TranslateService {
+ def translate(from:String,to:String,text:String):Future[String]
+ def translateAll(from:String,to:String,texts:Seq[String]):Future[Seq[String]]
+}
diff --git a/server/src/main/twirl/ch/wsl/box/templates/index.scala.html b/server/src/main/twirl/ch/wsl/box/templates/index.scala.html
index 20f839908..5723038f9 100644
--- a/server/src/main/twirl/ch/wsl/box/templates/index.scala.html
+++ b/server/src/main/twirl/ch/wsl/box/templates/index.scala.html
@@ -1,35 +1,23 @@
-@import org.webjars.WebJarAssetLocator
-@import scala.util.Try
-@import scala.util.Random
@import ch.wsl.box.viewmodel.MatomoConfig
-@(version:String,uiModule:String,enableRedactor:Boolean,devServer:Boolean,basePath:String,color:String,matomo:Option[MatomoConfig])
-
+@(version:String,uiModule:String,basePath:String,color:String,matomo:Option[MatomoConfig])
-
+
+
+
-
-
-
-
+
+
-
-@* NOT READY @if(!devServer) {*@
-@* *@
-@* }*@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- @for(group <- Seq("latin","latin-ext")) {
- @for( width <- Seq("300","400","600","700")) {
-
- }
- }
-
-
-
-
-
-
-
- @if(enableRedactor) {
-
-
- }
-
-
-
- @if(devServer) {
-
-
- } else {
-
- }
-
-
-
-
+
-
+
+
@matomo.map{ m =>
@@ -109,8 +55,9 @@
}
-
+
+
diff --git a/server/src/test/scala/ch/wsl/box/rest/BoxFormAdminSpec.scala b/server/src/test/scala/ch/wsl/box/rest/BoxFormAdminSpec.scala
index 136a7b14f..91c878f5e 100644
--- a/server/src/test/scala/ch/wsl/box/rest/BoxFormAdminSpec.scala
+++ b/server/src/test/scala/ch/wsl/box/rest/BoxFormAdminSpec.scala
@@ -21,7 +21,7 @@ class BoxFormAdminSpec extends BaseSpec {
"Admin Box schema form" should "handled" in withServices { implicit services =>
- implicit val session = BoxSession(CurrentUser(DbInfo(services.connection.adminUser,services.connection.adminUser,Seq()),UserInfo(services.connection.adminUser,services.connection.adminUser,None,Seq(),Json.Null)))
+ implicit val session = BoxSession(CurrentUser(DbInfo(services.connection.adminUser,services.connection.adminUser,Seq()),UserInfo(services.connection.adminUser,services.connection.adminUser,services.connection.adminUser,None,Seq(),Json.Null)))
implicit val bdb = FullDatabase(services.connection.adminDB,services.connection.adminDB)
for{
diff --git a/server/src/test/scala/ch/wsl/box/rest/FormMetadataFactorySpec.scala b/server/src/test/scala/ch/wsl/box/rest/FormMetadataFactorySpec.scala
index 2f5af2e74..297da1778 100644
--- a/server/src/test/scala/ch/wsl/box/rest/FormMetadataFactorySpec.scala
+++ b/server/src/test/scala/ch/wsl/box/rest/FormMetadataFactorySpec.scala
@@ -24,8 +24,8 @@ class FormMetadataFactorySpec extends BaseSpec {
"Form metadata" should "not include roles specific fields" in withServices[Assertion] { implicit services =>
- val userWithRoles = CurrentUser(DbInfo("test","test",Seq("testRole")),UserInfo("test","test",None,Seq("testRole"),Json.Null))
- val userWithoutRoles = CurrentUser(DbInfo("test2","test2",Seq()),UserInfo("test2","test2",None,Seq(),Json.Null))
+ val userWithRoles = CurrentUser(DbInfo("test","test",Seq("testRole")),UserInfo("test","test","test",None,Seq("testRole"),Json.Null))
+ val userWithoutRoles = CurrentUser(DbInfo("test2","test2",Seq()),UserInfo("test2","test2","test2",None,Seq(),Json.Null))
implicit val up = UserProfile(services.connection.adminUser,services.connection.adminUser)
diff --git a/shared/src/main/scala/ch/wsl/box/model/shared/GeoJson.scala b/shared/src/main/scala/ch/wsl/box/model/shared/GeoJson.scala
index 6143bb934..01eb3fa7c 100644
--- a/shared/src/main/scala/ch/wsl/box/model/shared/GeoJson.scala
+++ b/shared/src/main/scala/ch/wsl/box/model/shared/GeoJson.scala
@@ -74,6 +74,10 @@ object GeoJson {
override def removeSimple(toDelete: SingleGeometry): Option[Geometry] = if(toDelete == this) None else Some(this)
}
+ sealed trait GeometryObject {
+ def name:String
+ }
+
// geojson geometry from postgis
// {"type":"Point","crs":{"type":"name","properties":{"name":"EPSG:21781"}},"coordinates":[720000,112000]}
sealed trait Geometry {
@@ -116,33 +120,41 @@ object GeoJson {
}
- case object Empty extends SingleGeometry {
+ case object Empty extends SingleGeometry with GeometryObject {
+
+ override val name: String = "EMPTY"
override def crs: CRS = CRS("EPSG:0")
- override def geomName: String = "EMPTY"
+ override def geomName: String = name
override def toString(precision: Double): String = "Empty"
override def convert(f: Coordinates => Coordinates, crs: CRS): Geometry = this
}
+ object Point extends GeometryObject {
+ override val name: String = "POINT"
+ }
case class Point(coordinates: Coordinates, crs:CRS) extends SingleGeometry {
override def allCoordinates: Seq[Coordinates] = Seq(coordinates)
- override def geomName: String = "POINT"
+ override def geomName: String = Point.name
override def toString(precision:Double): String = s"$geomName(${coordinates.toString(precision)})"
override def convert(f: Coordinates => Coordinates,crs:CRS): Point = Point(f(coordinates),crs)
}
+ object LineString extends GeometryObject {
+ override val name: String = "LINESTRING"
+ }
case class LineString(coordinates: Seq[Coordinates], crs:CRS) extends SingleGeometry {
override def allCoordinates: Seq[Coordinates] = coordinates
- override def geomName: String = "LINESTRING"
+ override def geomName: String = LineString.name
override def toString(precision:Double): String = s"$geomName(${coordinates.map(_.toString(precision)).mkString(",")})"
@@ -151,11 +163,12 @@ object GeoJson {
override def convert(f: Coordinates => Coordinates,crs:CRS): LineString = LineString(coordinates.map(f),crs)
}
+
case class MultiPoint(coordinates: Seq[Coordinates], crs:CRS) extends Geometry {
override def allCoordinates: Seq[Coordinates] = coordinates
- override def geomName: String = "MULTIPOINT"
+ override def geomName: String = MultiPoint.name
override def toString(precision:Double): String = s"$geomName(${coordinates.map(_.toString(precision)).mkString("(","),(",")")})"
@@ -169,7 +182,9 @@ object GeoJson {
}
- object MultiPoint {
+ object MultiPoint extends GeometryObject {
+ override val name: String = "MULTIPOINT"
+
def fromPoints(points:Seq[Point]) = {
val crs = points.map(_.crs).distinct
if (crs.length != 1) throw new Exception("Can't handle different CRS in the same geometry")
@@ -181,7 +196,7 @@ object GeoJson {
override def allCoordinates: Seq[Coordinates] = coordinates.flatten
- override def geomName: String = "MULTILINESTRING"
+ override def geomName: String = MultiLineString.name
override def toString(precision:Double): String = s"$geomName(${coordinates.map(_.map(_.toString(precision)).mkString(",")).mkString("(","),(",")")})"
@@ -195,7 +210,10 @@ object GeoJson {
}
- object MultiLineString {
+ object MultiLineString extends GeometryObject {
+
+ override val name: String = "MULTILINESTRING"
+
def fromLines(lines:Seq[LineString]) = {
val crs = lines.map(_.crs).distinct
if(crs.length != 1) throw new Exception("Can't handle different CRS in the same geometry")
@@ -203,11 +221,15 @@ object GeoJson {
}
}
+ object Polygon extends GeometryObject {
+ override val name: String = "POLYGON"
+ }
+
case class Polygon(coordinates: Seq[Seq[Coordinates]], crs:CRS) extends SingleGeometry {
override def allCoordinates: Seq[Coordinates] = coordinates.flatten
- override def geomName: String = "POLYGON"
+ override def geomName: String = Polygon.name
override def toString(precision:Double): String = s"$geomName (${coordinates.map(_.map(_.toString(precision)).mkString(",")).mkString("(","),(",")")})"
@@ -220,7 +242,7 @@ object GeoJson {
override def allCoordinates: Seq[Coordinates] = coordinates.flatMap(_.flatten)
- override def geomName: String = "MULTIPOLYGON"
+ override def geomName: String = MultiPolygon.name
override def toString(precision:Double): String = s"$geomName (${coordinates.map(_.map(_.map(_.toString(precision)).mkString(",")).mkString("(","),(",")")).mkString("(","),(",")")})"
@@ -234,7 +256,10 @@ object GeoJson {
}
- object MultiPolygon {
+ object MultiPolygon extends GeometryObject {
+
+ override val name: String = "MULTIPOLYGON"
+
def fromPolygons(polygons: Seq[Polygon]) = {
val crs = polygons.map(_.crs).distinct
if (crs.length != 1) throw new Exception("Can't handle different CRS in the same geometry")
@@ -298,9 +323,9 @@ object GeoJson {
}
geoms.filterNot(_.geomName == Empty.geomName).map(_.geomName).distinct.toList match {
- case "POINT" :: Nil => MultiPoint.fromPoints(geoms.map{ case p:Point => p})
- case "LINESTRING" :: Nil => MultiLineString.fromLines(geoms.map{ case p:LineString => p})
- case "POLYGON" :: Nil => MultiPolygon.fromPolygons(geoms.map{ case p:Polygon => p})
+ case Point.name :: Nil => MultiPoint.fromPoints(geoms.map{ case p:Point => p})
+ case LineString.name :: Nil => MultiLineString.fromLines(geoms.map{ case p:LineString => p})
+ case Polygon.name :: Nil => MultiPolygon.fromPolygons(geoms.map{ case p:Polygon => p})
case _ => GeometryCollection(geoms,crs)
}
}
diff --git a/shared/src/main/scala/ch/wsl/box/model/shared/JSONMetadata.scala b/shared/src/main/scala/ch/wsl/box/model/shared/JSONMetadata.scala
index 440b358d5..ee15e5ee4 100755
--- a/shared/src/main/scala/ch/wsl/box/model/shared/JSONMetadata.scala
+++ b/shared/src/main/scala/ch/wsl/box/model/shared/JSONMetadata.scala
@@ -51,6 +51,14 @@ case class JSONMetadata(
// }.dropWhile(_ == 0).headOption.getOrElse(0)
// }
def table:Seq[JSONField] = tabularFields.flatMap(tf => fields.find(_.name == tf))
+ def exportTable:Seq[JSONField] = {
+ val r = exportFields.flatMap(tf => fields.find(_.name == tf))
+ r
+ }
+ def preselectedTable:Seq[JSONField] = params.flatMap(_.jsOpt("preselectedFields").flatMap(_.as[Seq[String]].toOption)) match {
+ case Some(preselected) => preselected.flatMap(tf => fields.find(_.name == tf))
+ case None => table
+ }
def tableLookupFields = table.filter(_.lookup.isDefined)
lazy val keyFields:Seq[JSONField] = keys.flatMap(k => fields.find(_.name == k))
diff --git a/shared/src/main/scala/ch/wsl/box/model/shared/JSONQuery.scala b/shared/src/main/scala/ch/wsl/box/model/shared/JSONQuery.scala
index c0f9f281a..496783176 100755
--- a/shared/src/main/scala/ch/wsl/box/model/shared/JSONQuery.scala
+++ b/shared/src/main/scala/ch/wsl/box/model/shared/JSONQuery.scala
@@ -19,7 +19,8 @@ case class JSONQuery(
filter:List[JSONQueryFilter],
sort:List[JSONSort],
paging:Option[JSONQueryPaging],
- sqlWhere:Option[String] = None
+ sqlWhere:Option[String] = None,
+ fields:Option[Seq[String]] = None
){
def validatedWhere = sqlWhere.map(_.replaceAll("insert ","not valid")
diff --git a/shared/src/main/scala/ch/wsl/box/model/shared/SharedLabels.scala b/shared/src/main/scala/ch/wsl/box/model/shared/SharedLabels.scala
index d802218dc..1992168de 100644
--- a/shared/src/main/scala/ch/wsl/box/model/shared/SharedLabels.scala
+++ b/shared/src/main/scala/ch/wsl/box/model/shared/SharedLabels.scala
@@ -226,6 +226,7 @@ object SharedLabels extends LabelsCollection {
def confirmRevert = "table.confirmRevert"
def csv = "table.csv"
def xls = "table.xls"
+ def pdf = "table.pdf"
def importxls = "table.importxls"
def shp = "table.shp"
def geopackage = "table.geopackage"
@@ -241,6 +242,7 @@ object SharedLabels extends LabelsCollection {
confirmRevert,
csv,
xls,
+ pdf,
importxls,
shp,
geopackage
@@ -272,10 +274,12 @@ object SharedLabels extends LabelsCollection {
def search = "popup.search"
def close = "popup.close"
def remove = "popup.remove"
+ def back = "popup.back"
def all = Seq(
search,
close,
- remove
+ remove,
+ back
)
}
diff --git a/shared/src/main/scala/ch/wsl/box/shared/utils/JSONUtils.scala b/shared/src/main/scala/ch/wsl/box/shared/utils/JSONUtils.scala
index 9a9ad6a94..331bf8d73 100755
--- a/shared/src/main/scala/ch/wsl/box/shared/utils/JSONUtils.scala
+++ b/shared/src/main/scala/ch/wsl/box/shared/utils/JSONUtils.scala
@@ -24,7 +24,7 @@ object JSONUtils extends Logging {
Try {
val json:Json = typ match {
case JSONFieldTypes.NUMBER => value.toDouble.asJson
- case JSONFieldTypes.INTEGER => value.toLong.asJson
+ case JSONFieldTypes.INTEGER => value.toDouble.toLong.asJson
case JSONFieldTypes.BOOLEAN => Json.fromBoolean(value.toBoolean)
case JSONFieldTypes.JSON => parser.parse(value) match {
case Left(value) => throw new Exception(value.message)
diff --git a/stats.json b/stats.json
new file mode 100644
index 000000000..f1f7d3b08
--- /dev/null
+++ b/stats.json
@@ -0,0 +1 @@
+{"hash":"e9b959a72cbce9303134","version":"5.89.0","time":193,"builtAt":1756460502186,"publicPath":"auto","outputPath":"/home/minettiandrea/Dev/Box/box/dist","assetsByChunkName":{"main":["main.js"]},"assets":[{"type":"asset","name":"main.js","size":0,"emitted":false,"comparedForEmit":false,"cached":true,"info":{"javascriptModule":false,"minimized":true},"chunkNames":["main"],"chunkIdHints":[],"auxiliaryChunkNames":[],"auxiliaryChunkIdHints":[],"related":{},"chunks":[179],"auxiliaryChunks":[],"isOverSizeLimit":false}],"chunks":[{"rendered":true,"initial":true,"entry":true,"recorded":false,"size":0,"sizes":{},"names":["main"],"idHints":[],"runtime":["main"],"files":["main.js"],"auxiliaryFiles":[],"hash":"9a09e9618fcdd19494c3","childrenByOrder":{},"id":179,"siblings":[],"parents":[],"children":[],"modules":[],"origins":[{"module":"","moduleIdentifier":"","moduleName":"","loc":"main","request":"./src"}]}],"modules":[],"entrypoints":{"main":{"name":"main","chunks":[179],"assets":[{"name":"main.js"}],"filteredAssets":0,"assetsSize":null,"auxiliaryAssets":[],"filteredAuxiliaryAssets":0,"auxiliaryAssetsSize":0,"children":{},"childAssets":{},"isOverSizeLimit":false}},"namedChunkGroups":{"main":{"name":"main","chunks":[179],"assets":[{"name":"main.js"}],"filteredAssets":0,"assetsSize":null,"auxiliaryAssets":[],"filteredAuxiliaryAssets":0,"auxiliaryAssetsSize":0,"children":{},"childAssets":{},"isOverSizeLimit":false}},"errors":[{"loc":"main","message":"Module not found: Error: Can't resolve './src' in '/home/minettiandrea/Dev/Box/box'","details":"resolve './src' in '/home/minettiandrea/Dev/Box/box'\n using description file: /home/minettiandrea/Dev/Box/box/package.json (relative path: .)\n Field 'browser' doesn't contain a valid alias configuration\n using description file: /home/minettiandrea/Dev/Box/box/package.json (relative path: ./src)\n no extension\n Field 'browser' doesn't contain a valid alias configuration\n /home/minettiandrea/Dev/Box/box/src doesn't exist\n .js\n Field 'browser' doesn't contain a valid alias configuration\n /home/minettiandrea/Dev/Box/box/src.js doesn't exist\n .json\n Field 'browser' doesn't contain a valid alias configuration\n /home/minettiandrea/Dev/Box/box/src.json doesn't exist\n .wasm\n Field 'browser' doesn't contain a valid alias configuration\n /home/minettiandrea/Dev/Box/box/src.wasm doesn't exist\n as directory\n /home/minettiandrea/Dev/Box/box/src doesn't exist","stack":"ModuleNotFoundError: Module not found: Error: Can't resolve './src' in '/home/minettiandrea/Dev/Box/box'\n at /home/minettiandrea/Dev/Box/box/node_modules/webpack/lib/Compilation.js:2022:28\n at /home/minettiandrea/Dev/Box/box/node_modules/webpack/lib/NormalModuleFactory.js:817:13\n at eval (eval at create (/home/minettiandrea/Dev/Box/box/node_modules/tapable/lib/HookCodeFactory.js:33:10), :10:1)\n at /home/minettiandrea/Dev/Box/box/node_modules/webpack/lib/NormalModuleFactory.js:275:22\n at eval (eval at create (/home/minettiandrea/Dev/Box/box/node_modules/tapable/lib/HookCodeFactory.js:33:10), :9:1)\n at /home/minettiandrea/Dev/Box/box/node_modules/webpack/lib/NormalModuleFactory.js:448:22\n at /home/minettiandrea/Dev/Box/box/node_modules/webpack/lib/NormalModuleFactory.js:118:11\n at /home/minettiandrea/Dev/Box/box/node_modules/webpack/lib/NormalModuleFactory.js:689:25\n at /home/minettiandrea/Dev/Box/box/node_modules/webpack/lib/NormalModuleFactory.js:893:8\n at /home/minettiandrea/Dev/Box/box/node_modules/webpack/lib/NormalModuleFactory.js:1013:5"}],"errorsCount":1,"warnings":[{"message":"configuration\nThe 'mode' option has not been set, webpack will fallback to 'production' for this value.\nSet 'mode' option to 'development' or 'production' to enable defaults for each environment.\nYou can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/","stack":"NoModeWarning: configuration\nThe 'mode' option has not been set, webpack will fallback to 'production' for this value.\nSet 'mode' option to 'development' or 'production' to enable defaults for each environment.\nYou can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/\n at /home/minettiandrea/Dev/Box/box/node_modules/webpack/lib/WarnNoModeSetPlugin.js:20:30\n at Hook.eval [as call] (eval at create (/home/minettiandrea/Dev/Box/box/node_modules/tapable/lib/HookCodeFactory.js:19:10), :21:1)\n at Hook.CALL_DELEGATE [as _call] (/home/minettiandrea/Dev/Box/box/node_modules/tapable/lib/Hook.js:14:14)\n at Compiler.newCompilation (/home/minettiandrea/Dev/Box/box/node_modules/webpack/lib/Compiler.js:1125:30)\n at /home/minettiandrea/Dev/Box/box/node_modules/webpack/lib/Compiler.js:1170:29\n at Hook.eval [as callAsync] (eval at create (/home/minettiandrea/Dev/Box/box/node_modules/tapable/lib/HookCodeFactory.js:33:10), :6:1)\n at Hook.CALL_ASYNC_DELEGATE [as _callAsync] (/home/minettiandrea/Dev/Box/box/node_modules/tapable/lib/Hook.js:18:14)\n at Compiler.compile (/home/minettiandrea/Dev/Box/box/node_modules/webpack/lib/Compiler.js:1165:28)\n at /home/minettiandrea/Dev/Box/box/node_modules/webpack/lib/Compiler.js:524:12\n at Compiler.readRecords (/home/minettiandrea/Dev/Box/box/node_modules/webpack/lib/Compiler.js:989:5)"}],"warningsCount":1,"children":[]}
\ No newline at end of file