From 26237319ff042f52455e20c3038b21fa84007a04 Mon Sep 17 00:00:00 2001 From: Andrea Minetti Date: Fri, 29 May 2026 13:45:05 +0200 Subject: [PATCH 1/7] Layout improvements --- .../ch/wsl/box/client/styles/BoxStyle.scala | 5 + .../wsl/box/client/styles/GlobalStyles.scala | 24 ++- .../box/client/views/EntityTableView.scala | 163 ++++++++++-------- .../views/components/table/FilterBarDyn.scala | 6 +- 4 files changed, 121 insertions(+), 77 deletions(-) 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 index 5f9fbd15..c184f9bc 100644 --- a/client/src/main/scala/ch/wsl/box/client/styles/BoxStyle.scala +++ b/client/src/main/scala/ch/wsl/box/client/styles/BoxStyle.scala @@ -14,6 +14,7 @@ trait BoxStyle { val inputInvalid: StyleA val spaceBetween: StyleA val topTableContainer: StyleA + val topBarContainer: StyleA val flexContainer: StyleA val sidebarRightContent: StyleA val sidebar: StyleA @@ -152,7 +153,11 @@ trait BoxStyle { val showHide:StyleA val error:StyleA val iconBig:StyleA + val filterDynBar:StyleA val filterBlock:StyleA + val filterBlockTitle:StyleA + val tableTitle:StyleA + val tableMainActions: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 485e0593..13d43c72 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 @@ -382,9 +382,14 @@ class GlobalStyles(settings:Settings,conf:StyleConf) extends StyleSheet.Inline() ), justifyContent.spaceBetween, alignItems.center, - alignContent.center + alignContent.center, + boxShadow := "0px 2px 6px #999", + zIndex(5), + position.relative ) + override val topBarContainer = style() + override val flexContainer = style( display.flex, flexDirection.row, @@ -1751,6 +1756,11 @@ class GlobalStyles(settings:Settings,conf:StyleConf) extends StyleSheet.Inline() ) ) + override val filterDynBar =style( + display.flex, + alignItems.center + ) + val filterBlock = style( display.flex, alignItems.center, @@ -1773,7 +1783,17 @@ class GlobalStyles(settings:Settings,conf:StyleConf) extends StyleSheet.Inline() ) -// + override val filterBlockTitle = style( + fontWeight.bold, + marginLeft(15 px) + ) + + + override val tableTitle = style() + override val tableMainActions = style( + noMobile + ) + // // val mapPopup = style( // border.solid, // borderColor(conf.colors.main), 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 229fd47f..7978dc69 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 @@ -658,11 +658,30 @@ case class EntityTableView(model:ModelProperty[EntityTableModel], presenter:Enti override def getTemplate: generic.Modifier[Element] = div( produceWithNested(model.subProp(_.metadata)) { (metadata, nested) => + metadata match { - case Some(metadata) => if (presenter.hasGeometry()) { - div(new TwoPanelResize(presenter.defaultClose)(showMap(metadata), mainContent(metadata, nested))).render - } else { - div(mainContent(metadata, nested)).render + case Some(metadata) => { + val filterStyle:String = metadata.params.flatMap(_.getOpt("filterStyle")) match { + case Some(value) => value + case None => "all" + } + + val filterStyleDyn = filterStyle == "both" || filterStyle == "dyn" + val filterStyleAll = filterStyle == "both" || filterStyle == "all" + if (presenter.hasGeometry()) { + div( + topBar(metadata,nested,filterStyleDyn), + new TwoPanelResize(presenter.defaultClose)( + showMap(metadata), + tableContent(metadata,nested,filterStyleAll) + ) + ).render + } else { + div( + topBar(metadata,nested,filterStyleDyn), + mainContent(metadata, nested,filterStyleAll) + ).render + } } case None => div().render } @@ -678,7 +697,7 @@ case class EntityTableView(model:ModelProperty[EntityTableModel], presenter:Enti } else Seq() Seq( - div(ClientConf.style.noMobile)( + div(ClientConf.style.tableMainActions)( releaser(produce(model.subProp(_.name)) { m => div({ val out: Seq[Modifier] = (metadata.action.topTable(a) ++ adminActions).map { ta => @@ -793,7 +812,8 @@ case class EntityTableView(model:ModelProperty[EntityTableModel], presenter:Enti } - def tableContent(metadata:JSONMetadata,nested:Binding.NestedInterceptor,filterStyleAll:Boolean) = { + def tableContent(metadata:JSONMetadata,nested:Binding.NestedInterceptor,filterStyleAll:Boolean) = div( + ClientConf.style.fullHeightMax, ClientConf.style.tableHeaderFixed,{ nested(produceWithNested(model.subProp(_.selectedColumns)) { (columns,nested) => val table = new BoxTable(model.subSeq(_.rows),nested,ClientConf.style.tableView)( @@ -887,19 +907,12 @@ case class EntityTableView(model:ModelProperty[EntityTableModel], presenter:Enti }) table }) - } + }) - def mainContent(metadata:JSONMetadata,nested:Binding.NestedInterceptor): scalatags.generic.Modifier[Element] = { + def topBar(metadata:JSONMetadata,nested:Binding.NestedInterceptor,filterStyleDyn:Boolean) = { val disableSelection = metadata.params.exists(_.js("disableSelection") == Json.True) - val enableImport = metadata.params.exists(_.js("enableImport") == Json.True) - val filterStyle:String = metadata.params.flatMap(_.getOpt("filterStyle")) match { - case Some(value) => value - case None => "all" - } - val filterStyleDyn = filterStyle == "both" || filterStyle == "dyn" - val filterStyleAll = filterStyle == "both" || filterStyle == "all" val columnSelector = { @@ -912,14 +925,14 @@ case class EntityTableView(model:ModelProperty[EntityTableModel], presenter:Enti headerFactory = Some(_ => div(Labels.table.column_selection).render), bodyFactory = Some { nested => div( - metadata.nativeFields.filterNot(_.`type` == JSONFieldTypes.GEOMETRY).map{ c => - div( - Checkbox(localModel.bitransform(_.contains(c)){ - case true => localModel.get ++ Seq(c) - case false => localModel.get.filterNot(_ == c) - })()," ",c.title - ) - } + metadata.nativeFields.filterNot(_.`type` == JSONFieldTypes.GEOMETRY).map{ c => + div( + Checkbox(localModel.bitransform(_.contains(c)){ + case true => localModel.get ++ Seq(c) + case false => localModel.get.filterNot(_ == c) + })()," ",c.title + ) + } ).render }, @@ -960,62 +973,68 @@ case class EntityTableView(model:ModelProperty[EntityTableModel], presenter:Enti } + div(ClientConf.style.topBarContainer, - - div( - div(ClientConf.style.topTableContainer, + div(ClientConf.style.topTableContainer, + div(ClientConf.style.tableTitle, + h3(ClientConf.style.noMargin,ClientConf.style.formTitle, labelTitle(metadata)), + ), + div(display.flex,flexDirection.row,alignItems.center, + div( Labels.navigation.recordFound," ",nested(bind(model.subProp(_.ids.count)))), + nested(showIf(model.subProp(_.query).transform(presenter.isFiltered)){ + a(ClientConf.style.chipLink,Labels.navigation.recordsFiltered," \uD83D\uDDD9", onclick :+= ((e:Event) => { + presenter.resetFilters() + e.preventDefault() + }) + ).render + }), + if(!disableSelection) { + a(ClientConf.style.chipLink, Labels.navigation.selectAll, onclick :+= ((e: Event) => { + presenter.selectAll() + e.preventDefault() + })) + } else empty + ), + if(!disableSelection) { div( - h3(ClientConf.style.noMargin,ClientConf.style.formTitle, labelTitle(metadata)) - ), - div(display.flex,flexDirection.row,alignItems.center, - div( Labels.navigation.recordFound," ",nested(bind(model.subProp(_.ids.count)))), - nested(showIf(model.subProp(_.query).transform(presenter.isFiltered)){ - a(ClientConf.style.chipLink,Labels.navigation.recordsFiltered," \uD83D\uDDD9", onclick :+= ((e:Event) => { - presenter.resetFilters() + nested(showIf(model.subProp(_.selectedRow).transform(_.nonEmpty)) { + div( + Labels.navigation.recordsSelected, nested(bind(model.subProp(_.selectedRow).transform(_.length))), + presenter.actions(true).map(actionButton(model.subProp(_.selectedRow).get, ClientConf.style.chipLink)), + a(ClientConf.style.chipLink, Labels.navigation.removeSelection, " \uD83D\uDDD9", onclick :+= ((e: Event) => { + presenter.resetSelection() e.preventDefault() - }) + })), ).render - }), - if(!disableSelection) { - a(ClientConf.style.chipLink, Labels.navigation.selectAll, onclick :+= ((e: Event) => { - presenter.selectAll() - e.preventDefault() - })) - } else empty - ), - if(!disableSelection) { - div( - nested(showIf(model.subProp(_.selectedRow).transform(_.nonEmpty)) { - div( - Labels.navigation.recordsSelected, nested(bind(model.subProp(_.selectedRow).transform(_.length))), - presenter.actions(true).map(actionButton(model.subProp(_.selectedRow).get, ClientConf.style.chipLink)), - a(ClientConf.style.chipLink, Labels.navigation.removeSelection, " \uD83D\uDDD9", onclick :+= ((e: Event) => { - presenter.resetSelection() - e.preventDefault() - })), - ).render - }) - ) - } else empty, - div( display.flex, - exportDialog.render(nested, () => ExportParams( - metadata = metadata, - selectedFields = model.subProp(_.selectedColumns).get, - query = model.subProp(_.query).get.getOrElse(JSONQuery.limit(10000)) - )), - columnSelector, - pagination.render, + }) ) + } else empty, + div( display.flex, + exportDialog.render(nested, () => ExportParams( + metadata = metadata, + selectedFields = model.subProp(_.selectedColumns).get, + query = model.subProp(_.query).get.getOrElse(JSONQuery.limit(10000)) + )), + columnSelector, + pagination.render, + ) - ), + ), + if(filterStyleDyn) { + new FilterBarDyn(model.subProp(_.fieldQueries),model.subProp(_.lookups)).render(metadata.fields,metadata) + } else frag(), + ) + } + + def mainContent(metadata:JSONMetadata,nested:Binding.NestedInterceptor,filterStyleAll:Boolean): scalatags.generic.Modifier[Element] = { + + val enableImport = metadata.params.exists(_.js("enableImport") == Json.True) + + + + div( div(id := "box-table", ClientConf.style.tableHeaderFixed, - div( - ClientConf.style.fullHeightMax, - if(filterStyleDyn) { - new FilterBarDyn(model.subProp(_.fieldQueries),model.subProp(_.lookups)).render(metadata.fields,metadata) - } else frag(), - tableContent(metadata,nested,filterStyleAll) - ), + tableContent(metadata,nested,filterStyleAll), if(enableImport) { button(`type` := "button", onclick :+= presenter.importXLS, ClientConf.style.boxButton, Labels.entity.importxls) diff --git a/client/src/main/scala/ch/wsl/box/client/views/components/table/FilterBarDyn.scala b/client/src/main/scala/ch/wsl/box/client/views/components/table/FilterBarDyn.scala index b0ea27d1..097ba39d 100644 --- a/client/src/main/scala/ch/wsl/box/client/views/components/table/FilterBarDyn.scala +++ b/client/src/main/scala/ch/wsl/box/client/views/components/table/FilterBarDyn.scala @@ -37,9 +37,9 @@ class FilterBarDyn(val fieldQueries:Property[Seq[FieldQuery]], val lookups:Reada - div( display.flex,alignItems.center, + div( ClientConf.style.filterDynBar, //div(pre(bind(fieldQueries.transform(_.map(x => s"Field: ${x.field.name}, ${x.filterValue} , Sort: ${x.sort} ${x.sortOrder}").mkString("\n"))))), - div(Icons.filter,"Filters",fontWeight.bold,marginLeft := 15.px), + div(ClientConf.style.filterBlockTitle,Icons.filter,"Filters"), repeat(_filterFields) { (fieldProp) => div(ClientConf.style.filterBlock, Select(fieldProp,fieldQueries.transformToSeq(_.map(_.field)))(_.title), @@ -66,7 +66,7 @@ class FilterBarDyn(val fieldQueries:Property[Seq[FieldQuery]], val lookups:Reada button(Icons.plus,ClientConf.style.boxButton,onclick :+= ((e:Event) => { fieldQueries.get.find(_.filterValue.isEmpty).map(x => _filterFields.append(x.field)) })), - div(Icons.asc,"Sort",fontWeight.bold,marginLeft := 15.px), + div(Icons.asc,"Sort",ClientConf.style.filterBlockTitle), repeatWithIndex(_sortFields) { case (fieldProp,i,nested) => div(ClientConf.style.filterBlock, Select(fieldProp,fieldQueries.transformToSeq(_.map(_.field)))(_.title), From 422f60b189f36fdea87fb4fce582d3d7bd302b10 Mon Sep 17 00:00:00 2001 From: Andrea Minetti Date: Mon, 1 Jun 2026 12:26:48 +0200 Subject: [PATCH 2/7] Defaulting map on the right --- .../ch/wsl/box/client/views/EntityTableView.scala | 10 +++++----- .../client/views/components/ui/TwoPanelResize.scala | 11 ++++++----- 2 files changed, 11 insertions(+), 10 deletions(-) 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 7978dc69..34dca78f 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 @@ -628,7 +628,7 @@ case class EntityTableView(model:ModelProperty[EntityTableModel], presenter:Enti var map:Option[Div] = None - def showMap(metadata:JSONMetadata) = (show:ReadableProperty[Boolean]) => showIf(show){ + def showMap(metadata:JSONMetadata) = () => { if(presenter.hasGeometry()) { if(model.subProp(_.geoms).get.isEmpty) @@ -672,8 +672,8 @@ case class EntityTableView(model:ModelProperty[EntityTableModel], presenter:Enti div( topBar(metadata,nested,filterStyleDyn), new TwoPanelResize(presenter.defaultClose)( + tableContent(metadata,nested,filterStyleAll), showMap(metadata), - tableContent(metadata,nested,filterStyleAll) ) ).render } else { @@ -812,7 +812,7 @@ case class EntityTableView(model:ModelProperty[EntityTableModel], presenter:Enti } - def tableContent(metadata:JSONMetadata,nested:Binding.NestedInterceptor,filterStyleAll:Boolean) = div( + def tableContent(metadata:JSONMetadata,nested:Binding.NestedInterceptor,filterStyleAll:Boolean) = () => div( ClientConf.style.fullHeightMax, ClientConf.style.tableHeaderFixed,{ nested(produceWithNested(model.subProp(_.selectedColumns)) { (columns,nested) => val table = new BoxTable(model.subSeq(_.rows),nested,ClientConf.style.tableView)( @@ -907,7 +907,7 @@ case class EntityTableView(model:ModelProperty[EntityTableModel], presenter:Enti }) table }) - }) + }).render def topBar(metadata:JSONMetadata,nested:Binding.NestedInterceptor,filterStyleDyn:Boolean) = { @@ -1034,7 +1034,7 @@ case class EntityTableView(model:ModelProperty[EntityTableModel], presenter:Enti div( div(id := "box-table", ClientConf.style.tableHeaderFixed, - tableContent(metadata,nested,filterStyleAll), + tableContent(metadata,nested,filterStyleAll)(), if(enableImport) { button(`type` := "button", onclick :+= presenter.importXLS, ClientConf.style.boxButton, Labels.entity.importxls) 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 5af2aed9..eb7c7d99 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 = 500 + val leftDefaultWidth = 80 case class Style(conf:StyleConf) extends StyleSheet.Inline{ import dsl._ @@ -29,7 +29,7 @@ class TwoPanelResize(defaultClose:Boolean) { ) val containerLeft = style( - width(leftDefaultWidth px), + width(leftDefaultWidth %%), if(defaultClose) width.`0` else { media.maxWidth(600 px)( width.`0` //:= "calc(100% - 15px)" )}, @@ -87,9 +87,10 @@ class TwoPanelResize(defaultClose:Boolean) { import scalatags.JsDom.all._ + private def toShowable(m:Seq[Node]) = (show:ReadableProperty[Boolean]) => showIf(show){ m } - def apply(leftPanel:ReadableProperty[Boolean] => Binding,rightPanel:Modifier):Modifier = { + def apply(leftPanel:() => Node,rightPanel:() => Node):Modifier = { val open = Property(true) @@ -217,10 +218,10 @@ class TwoPanelResize(defaultClose:Boolean) { Seq[Modifier]( styleElement, div(style.container, - div(style.containerLeft,leftPanel(open)), + div(style.containerLeft,toShowable(leftPanel())(open)), resizer, div(style.containerRight, - rightPanel + rightPanel() ) ) ) From 2736afcb54d389deba05bf159160b8553001fad9 Mon Sep 17 00:00:00 2001 From: Andrea Minetti Date: Mon, 1 Jun 2026 13:09:23 +0200 Subject: [PATCH 3/7] Messages interface draft --- .../main/scala/ch/wsl/box/client/Module.scala | 4 ++- .../ch/wsl/box/client/services/Messages.scala | 28 +++++++++++++++++++ .../box/client/services/ServiceModule.scala | 1 + 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 client/src/main/scala/ch/wsl/box/client/services/Messages.scala 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 da9b0a67..b17c1c4b 100644 --- a/client/src/main/scala/ch/wsl/box/client/Module.scala +++ b/client/src/main/scala/ch/wsl/box/client/Module.scala @@ -1,5 +1,5 @@ package ch.wsl.box.client -import ch.wsl.box.client.services.{ClientSession, DataAccessObject, HttpClient, Navigator, Notification, NotificationChannel, NotificationWebSocket, Preferences, REST} +import ch.wsl.box.client.services.{ClientSession, DataAccessObject, HttpClient, Messages, MessagesPropertyImpl, Navigator, Notification, NotificationChannel, NotificationWebSocket, Preferences, REST} import ch.wsl.box.client.services.impl.{DaoLocalDbImpl, DaoPassthroughImpl, HttpClientImpl, PreferencesImpl, RestImpl} import ch.wsl.box.client.styles.{BoxStyle, BoxStyleFactory, GlobalStyleFactory} import ch.wsl.box.client.views.components.{BoxMainLayout, MainLayout} @@ -23,6 +23,7 @@ object Module { .bind[NotificationChannel].to[NotificationWebSocket] .bind[BoxStyleFactory].to[GlobalStyleFactory] .bind[MainLayout].to[BoxMainLayout] + .bind[Messages].to[MessagesPropertyImpl] val prod = newDesign .bind[HttpClient].to[HttpClientImpl] @@ -34,4 +35,5 @@ object Module { .bind[NotificationChannel].to[NotificationWebSocket] .bind[BoxStyleFactory].to[GlobalStyleFactory] .bind[MainLayout].to[BoxMainLayout] + .bind[Messages].to[MessagesPropertyImpl] } diff --git a/client/src/main/scala/ch/wsl/box/client/services/Messages.scala b/client/src/main/scala/ch/wsl/box/client/services/Messages.scala new file mode 100644 index 00000000..4ca518f6 --- /dev/null +++ b/client/src/main/scala/ch/wsl/box/client/services/Messages.scala @@ -0,0 +1,28 @@ +package ch.wsl.box.client.services + +import ch.wsl.box.model.shared.JSONID +import io.circe.Json +import io.udash.Registration +import io.udash.properties.single.Property + +sealed trait Message + +trait Messages { + def pub(m:Message):Unit + def sub(action:Message => Any):Registration +} + +object Messages { + case object Empty extends Message + case class RowHover(id:Option[JSONID],row:Json) extends Message +} + + +class MessagesPropertyImpl extends Messages { + + val prop:Property[Message] = Property(Messages.Empty) + + override def pub(m: Message): Unit = prop.set(m) + + override def sub(action: Message => Any): Registration = prop.listen(action) +} \ No newline at end of file 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 ac3d22d4..63fb091f 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 @@ -14,4 +14,5 @@ trait ServiceModule { val style = bind[BoxStyleFactory] val layout = bind[MainLayout] val preferences = bind[Preferences] + val messages = bind[Messages] } From 084742a2ad9275cde215f7882eac5ea7ee9b8df0 Mon Sep 17 00:00:00 2001 From: Andrea Minetti Date: Wed, 3 Jun 2026 14:58:45 +0200 Subject: [PATCH 4/7] Hover map element and filter extent improvements --- client/package.json | 4 +- .../ch/wsl/box/client/geo/MapUtils.scala | 77 ++++++++++++++++++- .../ch/wsl/box/client/services/Messages.scala | 4 +- .../box/client/views/EntityTableView.scala | 43 ++++++++--- .../box/client/views/components/MapList.scala | 60 ++++++++++++--- .../views/components/ui/TwoPanelResize.scala | 22 +++--- .../ch/wsl/box/rest/logic/FormActions.scala | 3 +- 7 files changed, 178 insertions(+), 35 deletions(-) diff --git a/client/package.json b/client/package.json index c796dfd6..9f80fe38 100644 --- a/client/package.json +++ b/client/package.json @@ -46,8 +46,8 @@ "jsts": "2.7.1", "jsuites": "5.9.1", "monaco-editor": "0.34.0", - "ol": "8.1.0", - "ol-ext": "4.0.11", + "ol": "10.9.0", + "ol-ext": "4.0.38", "popper.js": "1.16.1", "proj4": "2.9.1", "quill": "1.3.7", diff --git a/client/src/main/scala/ch/wsl/box/client/geo/MapUtils.scala b/client/src/main/scala/ch/wsl/box/client/geo/MapUtils.scala index f0782394..61811d92 100644 --- a/client/src/main/scala/ch/wsl/box/client/geo/MapUtils.scala +++ b/client/src/main/scala/ch/wsl/box/client/geo/MapUtils.scala @@ -13,10 +13,16 @@ import org.scalajs.dom import scalatags.JsDom.all.s import scribe.Logging import ch.wsl.typings.ol.coordinateMod.Coordinate +import ch.wsl.typings.ol.layerLayerMod.Layer +import ch.wsl.typings.ol.layerTileMod.TileLayer import ch.wsl.typings.ol.mapBrowserEventMod.MapBrowserEvent -import ch.wsl.typings.ol.{featureMod, formatGeoJSONMod, formatMod, geomGeometryMod, layerBaseMod, layerBaseTileMod, layerMod, mod, projMod, sourceMod, sourceWmtsMod} +import ch.wsl.typings.ol.renderEventMod.RenderEvent +import ch.wsl.typings.ol.styleCircleMod.CircleStyle +import ch.wsl.typings.ol.styleMod.{Circle, Stroke, Style} +import ch.wsl.typings.ol.{featureMod, formatGeoJSONMod, formatMod, geomGeometryMod, layerBaseMod, layerBaseTileMod, layerMod, mod, observableMod, projMod, sourceMod, sourceWmtsMod, styleCircleMod, styleFillMod, styleMod, styleStrokeMod, styleStyleMod} +import org.scalajs.dom.Event -import java.util.UUID +import java.util.{Date, UUID} import scala.concurrent.Promise import scala.scalajs.js import scala.scalajs.js.|._ @@ -263,6 +269,73 @@ object MapUtils extends Logging { ol } + var listenerKey:ch.wsl.typings.ol.eventsMod.EventsKey = null + + def stopFlashing() = { + ch.wsl.typings.ol.eventsMod.unlistenByKey(listenerKey) + } + + def flash(feature: ch.wsl.typings.ol.featureMod.Feature[_], map: ch.wsl.typings.ol.mod.Map,layer:ch.wsl.typings.ol.layerMod.Vector[_]): Unit = { + val period = 1000 + val start = new Date().getTime + + val flashGeom = feature.getGeometry().get.asInstanceOf[js.Dynamic].clone().asInstanceOf[ch.wsl.typings.ol.geomMod.Geometry] + val flashGeom2 = feature.getGeometry().get.asInstanceOf[js.Dynamic].clone().asInstanceOf[ch.wsl.typings.ol.geomMod.Geometry] + + + + def animate(event: RenderEvent): Unit = { + println("Animate") + val frameState = event.frameState + val elapsed = frameState.get.time - start + +// if (elapsed >= duration) { +// ch.wsl.typings.ol.eventsMod.unlistenByKey(listenerKey) +// return +// } + + val vectorContext = ch.wsl.typings.ol.renderMod.getVectorContext(event) + val vectorContext2 = ch.wsl.typings.ol.renderMod.getVectorContext(event) + val elapsedRatio = elapsed / period - (elapsed / period).toInt + // radius will be 5 at start and 20 at end. + val radius = ch.wsl.typings.ol.easingMod.easeOut(elapsedRatio) * 15 + 5 + val opacity = ch.wsl.typings.ol.easingMod.easeOut(1 - elapsedRatio) + + val style = new Style( ch.wsl.typings.ol.styleStyleMod.Options().setImage( + new Circle(styleCircleMod.Options(radius) + .setStroke(new Stroke( ch.wsl.typings.ol.styleStrokeMod.Options() + .setColor(s"rgba(85, 10, 33, $opacity)") + .setWidth(1 + opacity) + )) + ).asInstanceOf[ch.wsl.typings.ol.imageMod.default] + ) ) + + val styleFIX = new Style( ch.wsl.typings.ol.styleStyleMod.Options().setImage( + new Circle(styleCircleMod.Options(10) + .setStroke(new Stroke( ch.wsl.typings.ol.styleStrokeMod.Options() + .setColor(s"rgba(85,10,33, 1)") + .setWidth(1) + )) + .setFill(new styleMod.Fill(styleFillMod.Options().setColor("rgba(85,10,33,0.6)"))) + ).asInstanceOf[ch.wsl.typings.ol.imageMod.default] + ) ) + + vectorContext.setStyle(style) + vectorContext.drawGeometry(flashGeom) + + vectorContext.setStyle(styleFIX) + vectorContext.drawGeometry(flashGeom2) + + + // tell OpenLayers to continue postrender animation + map.render() + } + + listenerKey = layer.onInternal("postrender", (event) => animate(event.asInstanceOf[RenderEvent])).asInstanceOf[ch.wsl.typings.ol.eventsMod.EventsKey] + map.render() + + } + implicit class EnanchedMap(map: mod.Map) { diff --git a/client/src/main/scala/ch/wsl/box/client/services/Messages.scala b/client/src/main/scala/ch/wsl/box/client/services/Messages.scala index 4ca518f6..44a6650a 100644 --- a/client/src/main/scala/ch/wsl/box/client/services/Messages.scala +++ b/client/src/main/scala/ch/wsl/box/client/services/Messages.scala @@ -1,5 +1,6 @@ package ch.wsl.box.client.services +import ch.wsl.box.client.viewmodel.Row import ch.wsl.box.model.shared.JSONID import io.circe.Json import io.udash.Registration @@ -14,7 +15,8 @@ trait Messages { object Messages { case object Empty extends Message - case class RowHover(id:Option[JSONID],row:Json) extends Message + case class RowHover(row:Row) extends Message + case class RowOut(row:Row) extends Message } 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 34dca78f..b659ba93 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 @@ -3,6 +3,7 @@ package ch.wsl.box.client.views 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.services.Messages.{RowHover, RowOut} import ch.wsl.box.client.{Context, EntityFormState, EntityTableState, FormPageState} import ch.wsl.box.client.services.{BrowserConsole, ClientConf, Labels, Navigate, Navigation, Notification, PDF, TablePreference, UI} import ch.wsl.box.client.styles.Icons.Icon @@ -73,14 +74,14 @@ case class FieldQuery(field:JSONField, sort:String, sortOrder:Option[Int], filte case class EntityTableModel(name:String, kind:String, urlQuery:Option[JSONQuery], rows:Seq[Row], fieldQueries:Seq[FieldQuery], metadata:Option[JSONMetadata], selectedRow:Seq[JSONID], ids: IDsVM, pages:Int, access:TableAccess, - lookups:Seq[JSONLookups],query:Option[JSONQuery],geoms: GeoTypes.GeoData,extent:Option[Polygon],public:Boolean,selectedColumns:Seq[JSONField]) + lookups:Seq[JSONLookups],query:Option[JSONQuery],geoms: GeoTypes.GeoData,extent:Option[Polygon],extentFilter:Boolean,public:Boolean,selectedColumns:Seq[JSONField]) case class VMAction(code:String,action: JSONID => Future[Boolean],icon:Option[Icon],label:String,button_class:String = "primary",confirm:Option[String] = None, reloadAfter:Boolean = false) object EntityTableModel extends HasModelPropertyCreator[EntityTableModel]{ - def empty = EntityTableModel("","",None,Seq(),Seq(),None,Seq(),IDsVMFactory.empty,1, TableAccess(false,false,false),Seq(),None,Seq(),None,false,Seq()) + def empty = EntityTableModel("","",None,Seq(),Seq(),None,Seq(),IDsVMFactory.empty,1, TableAccess(false,false,false),Seq(),None,Seq(),None,false,false,Seq()) implicit val blank: Blank[EntityTableModel] = Blank.Simple(empty) } @@ -220,6 +221,7 @@ case class EntityTablePresenter(model:ModelProperty[EntityTableModel], onSelect: query = Some(query), geoms = Seq(), extent = None, + extentFilter = false, public = state.public, selectedColumns = services.preferences.table(metadata).flatMap(_.selectedFields.map(metadata.getFields)).getOrElse(metadata.preselectedTable) ) @@ -262,6 +264,14 @@ case class EntityTablePresenter(model:ModelProperty[EntityTableModel], onSelect: } model.subProp(_.extent).listen { extent => + if(model.subProp(_.extentFilter).get) { + reloadRows(1) + } else { + loadGeoms(extent) + } + } + + model.subProp(_.extentFilter).listen { extent => reloadRows(1) } @@ -392,13 +402,13 @@ case class EntityTablePresenter(model:ModelProperty[EntityTableModel], onSelect: var reloadCount = 0 // avoid out of order - def defaultClose = model.subProp(_.metadata).get.exists(_.params.exists(_.js("mapClosed") == Json.True)) + def leftOpen = Property(true) + def rightOpen = Property(model.subProp(_.metadata).get.exists(_.params.exists(_.js("mapClosed") == Json.True))) def loadGeoms(extent:Option[Polygon] = None) = { model.get.metadata.foreach{ m => Future.sequence(m.geomFields.map{ f => - val tableEntity = m.view.getOrElse(m.entity) - services.rest.geoData(EntityKind.ENTITY.kind, services.clientSession.lang(), tableEntity, f.name, GeoDataRequest(query(extent).limit(10000000),m.keys),model.subProp(_.public).get) + services.rest.geoData(m.kind, services.clientSession.lang(), m.name, f.name, GeoDataRequest(query(extent).limit(10000000),m.keys),model.subProp(_.public).get) }).foreach{ geoms => model.subProp(_.geoms).set(geoms.flatten) @@ -414,7 +424,7 @@ case class EntityTablePresenter(model:ModelProperty[EntityTableModel], onSelect: services.clientSession.loading.set(true) - val extent = model.subProp(_.extent).get + val extent = if(model.subProp(_.extentFilter).get) model.subProp(_.extent).get else None logger.info(s"reloading rows page: $page") logger.info("filterUpdateHandler "+filterUpdateHandler) @@ -434,7 +444,7 @@ case class EntityTablePresenter(model:ModelProperty[EntityTableModel], onSelect: //start request in parallel val csvRequest = services.data.list(model.subProp(_.kind).get, services.clientSession.lang(), model.subProp(_.name).get, q,model.subProp(_.public).get,model.subProp(_.metadata).get.get) val idsRequest = services.rest.ids(model.get.kind, services.clientSession.lang(), model.get.name, q,model.subProp(_.public).get) - if(hasGeometry() && !defaultClose) { + if(hasGeometry() && rightOpen.get) { loadGeoms(extent) } @@ -524,6 +534,16 @@ case class EntityTablePresenter(model:ModelProperty[EntityTableModel], onSelect: } } + def hoverRow(row: => Row) = (e:Event) => { + services.messages.pub(RowHover(row)) + e.preventDefault() + } + + def exitRow(row: => Row) = (e:Event) => { + services.messages.pub(RowOut(row)) + e.preventDefault() + } + def toggleSelection(row: => Row) = (e:Event) => { row.id.foreach { id => val currentSel = model.subProp(_.selectedRow).get @@ -641,7 +661,7 @@ case class EntityTableView(model:ModelProperty[EntityTableModel], presenter:Enti map match { case Some(m) => if (document.contains(m) && m.offsetHeight > 0) { observer.disconnect() - new MapList(m,metadata,presenter.model.subProp(_.geoms),presenter.clickOnMap,model.subProp(_.extent)) + new MapList(m,metadata,presenter.model.subProp(_.geoms),presenter.clickOnMap,model.subProp(_.extent),model.subProp(_.extentFilter)) } case None => observer.disconnect() } @@ -671,7 +691,7 @@ case class EntityTableView(model:ModelProperty[EntityTableModel], presenter:Enti if (presenter.hasGeometry()) { div( topBar(metadata,nested,filterStyleDyn), - new TwoPanelResize(presenter.defaultClose)( + new TwoPanelResize(presenter.leftOpen,presenter.rightOpen)( tableContent(metadata,nested,filterStyleAll), showMap(metadata), ) @@ -852,7 +872,10 @@ case class EntityTableView(model:ModelProperty[EntityTableModel], presenter:Enti val row = tr( id := ElementId.tableRow(el.get.id.map(_.asString).getOrElse("")), - ClientConf.style.rowStyle, onclick :+= presenter.toggleSelection(el.get), + ClientConf.style.rowStyle, + onmouseover :+= presenter.hoverRow(el.get), + onmouseout :+= presenter.exitRow(el.get), + onclick :+= presenter.toggleSelection(el.get), td(ClientConf.style.smallCells)( Offline(el.transform(_.isLocal)), ), 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 13be38bb..40eb38fd 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 @@ -1,6 +1,7 @@ package ch.wsl.box.client.views.components import ch.wsl.box.client.geo.{BoxMapConstants, BoxMapProjections, BoxOlMap, MapActions, MapGeolocation, MapParams, MapStyle, MapUtils} +import ch.wsl.box.client.services.Messages.{RowHover, RowOut} import ch.wsl.box.client.services.{BrowserConsole, ClientConf} import ch.wsl.box.client.styles.constants.StyleConstants import ch.wsl.box.client.utils.{Debounce, ElementId} @@ -18,7 +19,7 @@ import org.scalajs.dom.{Event, HTMLInputElement, MutationObserver, document, win import ch.wsl.typings.ol._ import ch.wsl.typings.ol.geomMod.Point import ch.wsl.typings.ol.mapMod.MapOptions -import ch.wsl.typings.ol.mod.{MapBrowserEvent, Overlay} +import ch.wsl.typings.ol.mod.{Feature, 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} @@ -30,7 +31,7 @@ import scalatags.JsDom.all._ import io.udash._ import io.udash.wrappers.jquery.jQ -class MapList(_div:Div,metadata:JSONMetadata,geoms:ReadableProperty[GeoTypes.GeoData],edit: String => Unit,extent:Property[Option[Polygon]]) extends BoxOlMap { +class MapList(_div:Div,metadata:JSONMetadata,geoms:ReadableProperty[GeoTypes.GeoData],edit: String => Unit,extent:Property[Option[Polygon]],extentFilter:Property[Boolean]) extends BoxOlMap { import ch.wsl.box.client.Context._ import ch.wsl.box.client.Context.Implicits._ @@ -66,8 +67,11 @@ class MapList(_div:Div,metadata:JSONMetadata,geoms:ReadableProperty[GeoTypes.Geo val mapDiv = div(width := 100.pct, height := 100.pct).render + //https://openlayers.org/en/latest/examples/tooltip-on-hover.html + // Add info popup + val dispatchElements:Property[Seq[JSONID]] = Property(Seq()) - val popupDiv = div(display.none,ClientConf.style.mapPopup, + val dispatchElementsDiv = div(display.none,ClientConf.style.mapPopup, ul( produce(dispatchElements) { _.map{ id => li( @@ -80,7 +84,7 @@ class MapList(_div:Div,metadata:JSONMetadata,geoms:ReadableProperty[GeoTypes.Geo _div.appendChild(mapDiv) - _div.appendChild(popupDiv) + _div.appendChild(dispatchElementsDiv) val map = new mod.Map(MapOptions() .setTarget(mapDiv) @@ -89,13 +93,25 @@ class MapList(_div:Div,metadata:JSONMetadata,geoms:ReadableProperty[GeoTypes.Geo - val overlay = new Overlay(overlayMod.Options().setElement(popupDiv)) + val overlayDispatch = new Overlay(overlayMod.Options().setElement(dispatchElementsDiv)) - map.addOverlay(overlay) + map.addOverlay(overlayDispatch) val geolocation = new MapGeolocation(map) + + def extentFilterControl = new controlMod.Control(controlControlMod.Options().setElement( + div( + `class` := "ol-control", + style := "top: 40px; right:10px; padding: 1px 6px", + Checkbox(extentFilter)() + ,"Filter" + ).render + )) + map.addControl(geolocation.control) + map.addControl(extentFilterControl) + override val mapActions: MapActions = new MapActions(Some(map),options.crs) @@ -119,8 +135,15 @@ class MapList(_div:Div,metadata:JSONMetadata,geoms:ReadableProperty[GeoTypes.Geo .setSource(vectorSource) .setStyle(style) ) - map.addLayer(featuresLayer) + val hoverLayer = new layerMod.Vector(layerBaseVectorMod.Options() + .setStyle(style) + .setZIndex(100) + .setSource(new sourceMod.Vector[geomGeometryMod.default](sourceVectorMod.Options())) + ) + + map.addLayer(featuresLayer) + map.addLayer(hoverLayer) val extentChange = Debounce(250.millis)((_: Unit) => { @@ -145,6 +168,10 @@ class MapList(_div:Div,metadata:JSONMetadata,geoms:ReadableProperty[GeoTypes.Geo if (extent.get.isEmpty && layers.nonEmpty) { val sourceExtent = vectorSource.getExtent() + + BrowserConsole.log(map.getSize()) + BrowserConsole.log(map.getLayers().getArray()) + map.getView().fit(sourceExtent,FitOptions().setPadding(js.Array(20.0,20.0,20.0,20.0))) if (!extentListenerInitialized) { @@ -182,18 +209,31 @@ class MapList(_div:Div,metadata:JSONMetadata,geoms:ReadableProperty[GeoTypes.Geo }) map.asInstanceOf[js.Dynamic].on(olStrings.singleclick, (e:MapBrowserEvent[_]) => { - jQ(popupDiv).hide() + jQ(dispatchElementsDiv).hide() val ids = MapUtils.toJsonId(map,metadata.keys,e) if(ids.length == 1) { edit(ids.head.asString) } else if(ids.length > 1) { println("Dispatch " + ids) dispatchElements.set(ids) - jQ(popupDiv).show() - overlay.setPosition(e.coordinate); + jQ(dispatchElementsDiv).show() + overlayDispatch.setPosition(e.coordinate); } }) + services.messages.sub{ + case RowHover(row) => { + val f = vectorSource.getFeatureById(row.id.map(_.asString).getOrElse("")).asInstanceOf[Feature[_]] + if(f != null) { + MapUtils.flash(f,map,hoverLayer) + } + } + case RowOut(row) => { + MapUtils.stopFlashing() + } + case _ => () + } + } 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 eb7c7d99..567e58ca 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 @@ -14,7 +14,7 @@ import scala.util.Random // Ref https://phuoc.ng/collection/html-dom/create-resizable-split-views/ -class TwoPanelResize(defaultClose:Boolean) { +class TwoPanelResize(leftOpen:Property[Boolean],rightOpen:Property[Boolean]) { val leftDefaultWidth = 80 @@ -29,8 +29,11 @@ class TwoPanelResize(defaultClose:Boolean) { ) val containerLeft = style( - width(leftDefaultWidth %%), - if(defaultClose) width.`0` else { media.maxWidth(600 px)( + if(leftOpen.get && rightOpen.get) + width(leftDefaultWidth %%) + else + width(100 %%), + if(!leftOpen.get) width.`0` else { media.maxWidth(600 px)( width.`0` //:= "calc(100% - 15px)" )}, flexShrink(0), @@ -92,7 +95,7 @@ class TwoPanelResize(defaultClose:Boolean) { def apply(leftPanel:() => Node,rightPanel:() => Node):Modifier = { - val open = Property(true) + val style = Style(ClientConf.styleConf) val styleElement = document.createElement("style") @@ -103,9 +106,10 @@ class TwoPanelResize(defaultClose:Boolean) { val closeId = s"close-${Random.alphanumeric.take(8)}" val openLabel = i(UdashIcons.FontAwesome.Solid.caretRight, id := openId).render val closeLabel = i(UdashIcons.FontAwesome.Solid.caretLeft, id := closeId).render - if(window.innerWidth < 600 || defaultClose) { // on mobile default not showing map + if(window.innerWidth < 600 ) { // on mobile default not showing map closeLabel.classList.add(style.hide.htmlClass) - open.set(false) + leftOpen.set(true) + rightOpen.set(false) } else { openLabel.classList.add(style.hide.htmlClass) } @@ -136,13 +140,13 @@ class TwoPanelResize(defaultClose:Boolean) { document.getElementById(openId).classList.add(style.hide.htmlClass) document.getElementById(closeId).classList.remove(style.hide.htmlClass) window.dispatchEvent(new Event("resize")) - open.set(true) + leftOpen.set(true) } else { rightSide.style.display = "block" leftSide.style.width = "0%" document.getElementById(openId).classList.remove(style.hide.htmlClass) document.getElementById(closeId).classList.add(style.hide.htmlClass) - open.set(false) + leftOpen.set(false) } }) @@ -218,7 +222,7 @@ class TwoPanelResize(defaultClose:Boolean) { Seq[Modifier]( styleElement, div(style.container, - div(style.containerLeft,toShowable(leftPanel())(open)), + div(style.containerLeft,toShowable(leftPanel())(leftOpen)), resizer, div(style.containerRight, rightPanel() 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 5320981e..f6cfff14 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 @@ -88,7 +88,8 @@ case class FormActions(metadata:JSONMetadata, override def fetchGeom(properties:Seq[String],field:String,query:JSONQuery):DBIO[Seq[(Option[JSONID],Json,Geometry)]] = for { q <- queryForm(query) - result <- jsonAction.fetchGeom((properties ++ metadata.keys).distinct,field, q) + viewActions = metadata.view.map(v => Registry().actions(v)).getOrElse(jsonAction) + result <- viewActions.fetchGeom((properties ++ metadata.keys).distinct,field, q) } yield result.map{ case (_,data,geom) => (JSONID.fromData(data,metadata),data,geom) } From 66fab2fdfa4fbd740d6623c89fdb4334eec340d7 Mon Sep 17 00:00:00 2001 From: Andrea Minetti Date: Fri, 5 Jun 2026 16:51:16 +0200 Subject: [PATCH 5/7] Improved map UX --- .../ch/wsl/box/client/geo/MapControls.scala | 4 +- .../wsl/box/client/geo/MapGeolocation.scala | 5 +- .../ch/wsl/box/client/geo/MapUtils.scala | 1 - .../ch/wsl/box/client/styles/Icons.scala | 29 +++- .../box/client/views/EntityTableView.scala | 3 +- .../components/JSONMetadataRenderer.scala | 2 +- .../box/client/views/components/MapList.scala | 136 +++++++++++++++--- .../views/components/ui/TwoPanelResize.scala | 91 +++++++----- .../widget/geo/MapPointWidget.scala | 2 +- 9 files changed, 205 insertions(+), 68 deletions(-) diff --git a/client/src/main/scala/ch/wsl/box/client/geo/MapControls.scala b/client/src/main/scala/ch/wsl/box/client/geo/MapControls.scala index b7078064..6ddc41f8 100644 --- a/client/src/main/scala/ch/wsl/box/client/geo/MapControls.scala +++ b/client/src/main/scala/ch/wsl/box/client/geo/MapControls.scala @@ -316,7 +316,7 @@ abstract class MapControls(params:MapControlsParams)(implicit ec:ExecutionContex } e.preventDefault() }) - )(Icons.target).render + )(Icons.target()).render } ttgpsButtonGoTo = tt el @@ -337,7 +337,7 @@ abstract class MapControls(params:MapControlsParams)(implicit ec:ExecutionContex } e.preventDefault() }) - )(Icons.target).render + )(Icons.target()).render } ttgpsButtonInsert = tt el 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 5d221643..af5ca5ac 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 @@ -1,5 +1,6 @@ package ch.wsl.box.client.geo +import ch.wsl.box.client.styles.Icons 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} @@ -50,7 +51,7 @@ class MapGeolocation(map:mod.Map) { } }) - def control = new controlMod.Control(controlControlMod.Options().setElement(div(`class` := "ol-control", style := "top: 10px; right:10px; padding: 1px 6px", input( + def control = div(padding := 3.px,display.flex,justifyContent.spaceAround,alignItems.center, input( `type`:="checkbox", onchange :+= {(e:Event) => if(e.target.asInstanceOf[HTMLInputElement].checked) { @@ -63,7 +64,7 @@ class MapGeolocation(map:mod.Map) { } - ).render ,"GPS").render)) + ).render,Icons.target(24,24)).render } diff --git a/client/src/main/scala/ch/wsl/box/client/geo/MapUtils.scala b/client/src/main/scala/ch/wsl/box/client/geo/MapUtils.scala index 61811d92..ec1e19a1 100644 --- a/client/src/main/scala/ch/wsl/box/client/geo/MapUtils.scala +++ b/client/src/main/scala/ch/wsl/box/client/geo/MapUtils.scala @@ -285,7 +285,6 @@ object MapUtils extends Logging { def animate(event: RenderEvent): Unit = { - println("Animate") val frameState = event.frameState val elapsed = frameState.get.time - start diff --git a/client/src/main/scala/ch/wsl/box/client/styles/Icons.scala b/client/src/main/scala/ch/wsl/box/client/styles/Icons.scala index 6c7cda8f..d8440522 100644 --- a/client/src/main/scala/ch/wsl/box/client/styles/Icons.scala +++ b/client/src/main/scala/ch/wsl/box/client/styles/Icons.scala @@ -239,9 +239,9 @@ object Icons { |""".stripMargin) //http://simpleicon.com/wp-content/uploads/target1.svg - val target:Icon = raw( + def target(w:Int = 16,h:Int = 16):Icon = raw( s""" - | + | | | | @@ -409,4 +409,29 @@ object Icons { | |""".stripMargin) + //https://icons.getbootstrap.com/icons/zoom-in/ + val zoom:Icon = raw( + s""" + | + | + | + | + | + |""".stripMargin + ) + + //https://www.svgrepo.com/svg/451040/layer-zoom-to + def layerZoom(w:Int = 16,h:Int = 16):Icon = raw( + s""" + | + |""".stripMargin + ) + + //https://www.svgrepo.com/svg/450856/extent-filter + def extentFilter(w:Int = 16,h:Int = 16):Icon = raw( + s""" + | + |""".stripMargin + ) + } 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 b659ba93..7c93bba3 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 @@ -2,6 +2,7 @@ package ch.wsl.box.client.views import ch.wsl.box.client.Context.services import ch.wsl.box.client.db.{DB, LocalRecord} +import ch.wsl.box.client.geo.MapUtils import ch.wsl.box.client.routes.Routes import ch.wsl.box.client.services.Messages.{RowHover, RowOut} import ch.wsl.box.client.{Context, EntityFormState, EntityTableState, FormPageState} @@ -403,7 +404,7 @@ case class EntityTablePresenter(model:ModelProperty[EntityTableModel], onSelect: def leftOpen = Property(true) - def rightOpen = Property(model.subProp(_.metadata).get.exists(_.params.exists(_.js("mapClosed") == Json.True))) + def rightOpen = Property(!model.subProp(_.metadata).get.exists(_.params.exists(_.js("mapClosed") == Json.True))) def loadGeoms(extent:Option[Polygon] = None) = { model.get.metadata.foreach{ m => 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 c6231019..d9f97d40 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 @@ -140,7 +140,7 @@ case class JSONMetadataRenderer(metadata: JSONMetadata, data: Property[Json], ch Future.sequence(blocks.filter(_.layoutBlock.flatMap(_.tabGroup).isEmpty).map(_.widget.afterRender())).map(_.forall(x => x)) } - override protected def show(nested:Binding.NestedInterceptor): JsDom.all.Modifier = renderJsonMetadata(false,nested) + override def show(nested:Binding.NestedInterceptor): JsDom.all.Modifier = renderJsonMetadata(false,nested) override def edit(nested:Binding.NestedInterceptor): JsDom.all.Modifier = renderJsonMetadata(true,nested) 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 40eb38fd..e07d61c6 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 @@ -3,10 +3,12 @@ package ch.wsl.box.client.views.components import ch.wsl.box.client.geo.{BoxMapConstants, BoxMapProjections, BoxOlMap, MapActions, MapGeolocation, MapParams, MapStyle, MapUtils} import ch.wsl.box.client.services.Messages.{RowHover, RowOut} import ch.wsl.box.client.services.{BrowserConsole, ClientConf} +import ch.wsl.box.client.styles.Icons import ch.wsl.box.client.styles.constants.StyleConstants import ch.wsl.box.client.utils.{Debounce, ElementId} +import ch.wsl.box.client.views.components.widget.WidgetCallbackActions import ch.wsl.box.model.shared.GeoJson.{Coordinates, Polygon} -import ch.wsl.box.model.shared.{GeoJson, GeoTypes, JSONID, JSONMetadata} +import ch.wsl.box.model.shared.{GeoJson, GeoTypes, JSONFieldTypes, JSONID, JSONMetadata, Layout} import ch.wsl.box.shared.utils.JSONUtils.EnhancedJson import io.circe.Json import org.scalajs.dom.html.Div @@ -17,6 +19,7 @@ import io.udash.{Property, ReadableProperty} import org.scalajs.dom import org.scalajs.dom.{Event, HTMLInputElement, MutationObserver, document, window} import ch.wsl.typings.ol._ +import ch.wsl.typings.ol.coordinateMod.Coordinate import ch.wsl.typings.ol.geomMod.Point import ch.wsl.typings.ol.mapMod.MapOptions import ch.wsl.typings.ol.mod.{Feature, MapBrowserEvent, Overlay} @@ -29,6 +32,7 @@ import scala.scalajs.js import scala.scalajs.js.{JSON, |} import scalatags.JsDom.all._ import io.udash._ +import io.udash.bindings.modifiers.Binding.NestedInterceptor import io.udash.wrappers.jquery.jQ class MapList(_div:Div,metadata:JSONMetadata,geoms:ReadableProperty[GeoTypes.GeoData],edit: String => Unit,extent:Property[Option[Polygon]],extentFilter:Property[Boolean]) extends BoxOlMap { @@ -65,26 +69,43 @@ class MapList(_div:Div,metadata:JSONMetadata,geoms:ReadableProperty[GeoTypes.Geo import scalacss.ScalatagsCss._ import io.udash.css._ - val mapDiv = div(width := 100.pct, height := 100.pct).render - + val infoData = Property(Json.Null) //https://openlayers.org/en/latest/examples/tooltip-on-hover.html // Add info popup + val infoDiv = div(display.none,ClientConf.style.mapPopup,width := 350.px, + JSONMetadataRenderer(metadata.copy(layout = Layout.fromFields(metadata.table.filterNot(_.`type` == JSONFieldTypes.GEOMETRY))),infoData,Seq(),Property(None),WidgetCallbackActions.noAction,Property(false),false).show(NestedInterceptor.Identity) + ).render + + val mapDiv = div(width := 100.pct, height := 100.pct, onmouseout :+= ((e:Event) => jQ(infoDiv).hide())).render + + + + + val dispatchElements:Property[Seq[JSONID]] = Property(Seq()) val dispatchElementsDiv = div(display.none,ClientConf.style.mapPopup, ul( produce(dispatchElements) { _.map{ id => li( - a(id.prettyPrint(metadata), onclick :+= ((e:Event) => edit(id.asString)) ) + a(id.prettyPrint(metadata), onclick :+= { (e: Event) => edit(id.asString) + + }) ).render } } ) ).render + val controlDiv = div( + style := "top: 10px; right:10px; padding: 3px; flex-direction: column; background-color: white; box-shadow: 0px 2px 3px #999; position: absolute; width: 60px", + display.flex + ).render + _div.appendChild(mapDiv) _div.appendChild(dispatchElementsDiv) + _div.appendChild(infoDiv) val map = new mod.Map(MapOptions() .setTarget(mapDiv) @@ -97,20 +118,56 @@ class MapList(_div:Div,metadata:JSONMetadata,geoms:ReadableProperty[GeoTypes.Geo map.addOverlay(overlayDispatch) + val overlayInfo = new Overlay(overlayMod.Options().setElement(infoDiv)) + + map.addOverlay(overlayInfo) + val geolocation = new MapGeolocation(map) + val vectorSource = new sourceMod.Vector[geomGeometryMod.default](sourceVectorMod.Options()) - def extentFilterControl = new controlMod.Control(controlControlMod.Options().setElement( - div( - `class` := "ol-control", - style := "top: 40px; right:10px; padding: 1px 6px", + def zoomToFeatures() = { + val sourceExtent = vectorSource.getExtent() + map.getView().fit(sourceExtent,FitOptions().setPadding(js.Array(20.0,20.0,20.0,20.0))) + } + + val extentFilterControl = div(padding := 3.px,display.flex,justifyContent.spaceAround,alignItems.center, Checkbox(extentFilter)() - ,"Filter" + ,Icons.extentFilter(24,24) + ).render + + + val zoomToFeaturesControl = div(padding := 3.px, + div( display.flex,alignItems.center,justifyContent.center, + button(ClientConf.style.boxIconButton,Icons.layerZoom(), onclick :+= {(e:Event) => + if(!extentFilter.get) + extent.set(None) + zoomToFeatures() + }) + ) ).render - )) - map.addControl(geolocation.control) - map.addControl(extentFilterControl) + + controlDiv.append(geolocation.control) + controlDiv.append(extentFilterControl) + controlDiv.append(zoomToFeaturesControl) + + + map.addControl(new controlMod.Control(controlControlMod.Options().setElement(controlDiv))) + + options.baseLayers.foreach {layers => + + val bl = baseLayer.bitransform(_.getOrElse(layers.head))(x => Some(x)) + + def baseLayerControl = new controlMod.Control(controlControlMod.Options().setElement( + div( + `class` := "ol-control", + style := "bottom: 10px; left:10px; padding: 1px 6px; background-color: transparent", + Select(bl,SeqProperty(layers))(_.name) + ).render + )) + map.addControl(baseLayerControl) + } override val mapActions: MapActions = new MapActions(Some(map),options.crs) @@ -130,7 +187,7 @@ class MapList(_div:Div,metadata:JSONMetadata,geoms:ReadableProperty[GeoTypes.Geo - val vectorSource = new sourceMod.Vector[geomGeometryMod.default](sourceVectorMod.Options()) + val featuresLayer = new layerMod.Vector(layerBaseVectorMod.Options() .setSource(vectorSource) .setStyle(style) @@ -147,7 +204,9 @@ class MapList(_div:Div,metadata:JSONMetadata,geoms:ReadableProperty[GeoTypes.Geo val extentChange = Debounce(250.millis)((_: Unit) => { - extent.set(Some(mapActions.calculateExtent(proj.default.crs))) + val newExtent = mapActions.calculateExtent(proj.default.crs) + if(MapUtils.area(Seq(newExtent)) > 0) // avoid setting blank offset when closing the map, in mobile that's relevant + extent.set(Some(newExtent)) }) var extentListenerInitialized = false @@ -167,12 +226,7 @@ class MapList(_div:Div,metadata:JSONMetadata,geoms:ReadableProperty[GeoTypes.Geo if (extent.get.isEmpty && layers.nonEmpty) { - val sourceExtent = vectorSource.getExtent() - - BrowserConsole.log(map.getSize()) - BrowserConsole.log(map.getLayers().getArray()) - - map.getView().fit(sourceExtent,FitOptions().setPadding(js.Array(20.0,20.0,20.0,20.0))) + zoomToFeatures() if (!extentListenerInitialized) { extentListenerInitialized = true @@ -208,8 +262,15 @@ class MapList(_div:Div,metadata:JSONMetadata,geoms:ReadableProperty[GeoTypes.Geo } }) + + map.asInstanceOf[js.Dynamic].on("change:size", ((e:Event) => { + extentChange() + })) + map.asInstanceOf[js.Dynamic].on(olStrings.singleclick, (e:MapBrowserEvent[_]) => { + dispatchElements.set(Seq()) jQ(dispatchElementsDiv).hide() + jQ(infoDiv).hide() val ids = MapUtils.toJsonId(map,metadata.keys,e) if(ids.length == 1) { edit(ids.head.asString) @@ -221,6 +282,41 @@ class MapList(_div:Div,metadata:JSONMetadata,geoms:ReadableProperty[GeoTypes.Geo } }) + val loadData = Debounce[(JSONID,Coordinate)](250.millis)({ case (id,coordinate) => services.rest.get(metadata.kind, metadata.lang, metadata.name, id, false).map { data => + infoData.set(data) + overlayInfo.setPosition(coordinate); + window.setTimeout(() => { + jQ(infoDiv).show() + },0) + + }}) + + var infoId:Option[JSONID] = None + + def pointerMove(e:MapBrowserEvent[_]) = { + if(dispatchElements.get.isEmpty && window.innerWidth > 650) { // don't show popup's on mobile + val ids = MapUtils.toJsonId(map, metadata.keys, e) + ids.headOption match { + case Some(id) => if (!infoId.contains(id)) { + infoData.set(Json.Null) + infoId = Some(id) + loadData(id,e.coordinate) + } else { + window.setTimeout(() => { + if(JSONID.fromData(infoData.get,metadata) == infoId) + jQ(infoDiv).show() + },0) + + } + case None => { + jQ(infoDiv).hide() + } + } + } + } + + map.asInstanceOf[js.Dynamic].on(olStrings.pointermove,x => pointerMove(x)) + services.messages.sub{ case RowHover(row) => { val f = vectorSource.getFeatureById(row.id.map(_.asString).getOrElse("")).asInstanceOf[Feature[_]] 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 567e58ca..d551fe1c 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 @@ -14,7 +14,7 @@ import scala.util.Random // Ref https://phuoc.ng/collection/html-dom/create-resizable-split-views/ -class TwoPanelResize(leftOpen:Property[Boolean],rightOpen:Property[Boolean]) { +class TwoPanelResize(leftOpen:Property[Boolean],rightOpen:Property[Boolean], changed: () => Unit = () => ()) { val leftDefaultWidth = 80 @@ -29,13 +29,12 @@ class TwoPanelResize(leftOpen:Property[Boolean],rightOpen:Property[Boolean]) { ) val containerLeft = style( - if(leftOpen.get && rightOpen.get) + if(window.innerWidth < 600) + width(100 %%) + else if(leftOpen.get && rightOpen.get) width(leftDefaultWidth %%) else - width(100 %%), - if(!leftOpen.get) width.`0` else { media.maxWidth(600 px)( - width.`0` //:= "calc(100% - 15px)" - )}, + width := "calc(100% - 15px)", flexShrink(0), backgroundColor.white, zIndex(2) @@ -83,9 +82,6 @@ class TwoPanelResize(leftOpen:Property[Boolean],rightOpen:Property[Boolean]) { userSelect.none ) - val hide = style( - display.none - ) } import scalatags.JsDom.all._ @@ -102,19 +98,17 @@ class TwoPanelResize(leftOpen:Property[Boolean],rightOpen:Property[Boolean]) { styleElement.innerText = style.render(cssStringRenderer, cssEnv) import io.udash.css.CssView._ - val openId = s"open-${Random.alphanumeric.take(8)}" - val closeId = s"close-${Random.alphanumeric.take(8)}" - val openLabel = i(UdashIcons.FontAwesome.Solid.caretRight, id := openId).render - val closeLabel = i(UdashIcons.FontAwesome.Solid.caretLeft, id := closeId).render + val rightLabel = div(i(UdashIcons.FontAwesome.Solid.caretRight)).render + val leftLabel = div(i(UdashIcons.FontAwesome.Solid.caretLeft)).render if(window.innerWidth < 600 ) { // on mobile default not showing map - closeLabel.classList.add(style.hide.htmlClass) leftOpen.set(true) rightOpen.set(false) - } else { - openLabel.classList.add(style.hide.htmlClass) } - val resizerLabel = p(style.resizerLabel, openLabel,closeLabel).render + val resizerLabel = p(style.resizerLabel, + showIf(leftOpen)(leftLabel), + showIf(rightOpen)(rightLabel) + ).render val resizer = div(style.resizer, @@ -128,25 +122,36 @@ class TwoPanelResize(leftOpen:Property[Boolean],rightOpen:Property[Boolean]) { var leftWidth = 0.0 var dragging = false - resizerLabel.addEventListener("click", (e: Event) => { - if(leftSide.getBoundingClientRect().width < 10) { - if(window.innerWidth < 600) { - leftSide.style.width = "100%" - rightSide.style.display = "none" - } else { - leftSide.style.width = leftDefaultWidth + "px" - } + def currentLeftWidthPct = leftSide.getBoundingClientRect().width / (resizer.parentNode.asInstanceOf[HTMLDivElement].getBoundingClientRect().width-15) * 100 + + leftLabel.addEventListener("click", (e: Event) => { + if(window.innerWidth < 600 || currentLeftWidthPct < 60) { + leftOpen.set(false) + rightOpen.set(true) + leftSide.style.width = "0%" + + } else { + leftOpen.set(true) + rightOpen.set(true) + leftSide.style.width = "50%" - document.getElementById(openId).classList.add(style.hide.htmlClass) - document.getElementById(closeId).classList.remove(style.hide.htmlClass) - window.dispatchEvent(new Event("resize")) + } + }) + + rightLabel.addEventListener("click", (e: Event) => { + if(window.innerWidth < 600 || currentLeftWidthPct > 50) { leftOpen.set(true) + rightOpen.set(false) + if(window.innerWidth < 600) + leftSide.style.width = "100%" + else + leftSide.style.width = "calc(100% - 15px)" + } else { - rightSide.style.display = "block" - leftSide.style.width = "0%" - document.getElementById(openId).classList.remove(style.hide.htmlClass) - document.getElementById(closeId).classList.add(style.hide.htmlClass) - leftOpen.set(false) + leftOpen.set(true) + rightOpen.set(true) + leftSide.style.width = "80%" + } }) @@ -158,13 +163,23 @@ class TwoPanelResize(leftOpen:Property[Boolean],rightOpen:Property[Boolean]) { var newLeftWidth = ((leftWidth + dx) * 100) / resizer.parentNode.asInstanceOf[HTMLDivElement].getBoundingClientRect().width newLeftWidth = math.max(0,math.min(newLeftWidth,100)) leftSide.style.width = newLeftWidth + "%" - if(newLeftWidth > 10) { - document.getElementById(openId).classList.add(style.hide.htmlClass) - document.getElementById(closeId).classList.remove(style.hide.htmlClass) + if(newLeftWidth < 5) { + leftOpen.set(false) + rightOpen.set(true) + } else if(newLeftWidth > 95) { + leftOpen.set(true) + rightOpen.set(false) } else { - document.getElementById(openId).classList.remove(style.hide.htmlClass) - document.getElementById(closeId).classList.add(style.hide.htmlClass) + leftOpen.set(true) + rightOpen.set(true) } +// if(newLeftWidth < 10) { +// document.getElementById(rightId).classList.remove(style.hide.htmlClass) +// document.getElementById(leftId).classList.remove(style.hide.htmlClass) +// } else if() { +// document.getElementById(openId).classList.remove(style.hide.htmlClass) +// document.getElementById(closeId).classList.add(style.hide.htmlClass) +// } } } diff --git a/client/src/main/scala/ch/wsl/box/client/views/components/widget/geo/MapPointWidget.scala b/client/src/main/scala/ch/wsl/box/client/views/components/widget/geo/MapPointWidget.scala index e73b2c57..0d4c86cd 100644 --- a/client/src/main/scala/ch/wsl/box/client/views/components/widget/geo/MapPointWidget.scala +++ b/client/src/main/scala/ch/wsl/box/client/views/components/widget/geo/MapPointWidget.scala @@ -234,7 +234,7 @@ case class MapPointWidget(params: WidgetParams) extends Widget with HasData with } e.preventDefault() // needed in order to avoid triggering the form validation } - )(Icons.target).render)._1 + )(Icons.target()).render)._1 private def showMap(modalStatus:Property[String],mod:Modifier*) = { WidgetUtils.addTooltip(Some("Show on map"))(a(BootstrapStyles.Button.btn, backgroundColor := scalacss.internal.Color.transparent.value, paddingTop := 0.px, paddingBottom := 0.px, onclick :+= ((e: Event) => { From d6285e2de7bcbd2f204548b175ddd6d49d56a22a Mon Sep 17 00:00:00 2001 From: Andrea Minetti Date: Mon, 15 Jun 2026 15:17:54 +0200 Subject: [PATCH 6/7] Lookup data draft --- db/box_migrations/BOX_R__lookup_data.sql | 38 +++++++++++++++++++ .../wsl/box/rest/routes/v1/PrivateArea.scala | 11 +++++- 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 db/box_migrations/BOX_R__lookup_data.sql diff --git a/db/box_migrations/BOX_R__lookup_data.sql b/db/box_migrations/BOX_R__lookup_data.sql new file mode 100644 index 00000000..70f222de --- /dev/null +++ b/db/box_migrations/BOX_R__lookup_data.sql @@ -0,0 +1,38 @@ + +CREATE OR REPLACE FUNCTION select_table_data( + schema text, entity text, fields text) + RETURNS SETOF RECORD + security invoker + LANGUAGE 'plpgsql' +AS $BODY$ +BEGIN + RETURN QUERY EXECUTE format('select to_jsonb(t) FROM (select %s from %I.%I) t ',fields,schema,entity); +END +$BODY$; +drop view if exists v_lookup_data; +create view v_lookup_data with ( security_invoker = true) as +with all_foreign_fields as (select foreign_entity entity, foreign_value_field field + from field + where foreign_entity is not null + union all + select foreign_entity, fkc + from field, + unnest(foreign_key_columns) fkc + where foreign_entity is not null + union all + select foreign_entity, unnest(fi.foreign_label_columns) + from field f + join field_i18n fi using (field_uuid) + where foreign_entity is not null), + agg as (select entity, + + array_agg(distinct field) fields, + '"' || string_agg(distinct field,'","') || '"' fields_txt + from all_foreign_fields aff + join metadata_columns mc on aff.entity = mc.table_name and aff.field = mc.column_name + group by entity + ) +select entity,fields,jsonb_agg(data) data from agg,select_table_data(coalesce((select value from conf where key='default-schema'),'public'),entity,fields_txt) d(data jsonb) +group by entity,fields; + +grant select on v_lookup_data to box_user; \ No newline at end of file diff --git a/server/src/main/scala/ch/wsl/box/rest/routes/v1/PrivateArea.scala b/server/src/main/scala/ch/wsl/box/rest/routes/v1/PrivateArea.scala index f3a75f51..5129f788 100644 --- a/server/src/main/scala/ch/wsl/box/rest/routes/v1/PrivateArea.scala +++ b/server/src/main/scala/ch/wsl/box/rest/routes/v1/PrivateArea.scala @@ -8,7 +8,7 @@ import akka.http.scaladsl.server.Directives import akka.http.scaladsl.server.Directives.{complete, get, path, pathPrefix} import akka.stream.Materializer import ch.wsl.box.model.Translations -import ch.wsl.box.model.shared.{BoxTranslationsFields, CSVTable, EntityKind, PDFTable, XLSTable} +import ch.wsl.box.model.shared.{BoxTranslationsFields, CSVTable, EntityKind, JSONQuery, PDFTable, XLSTable} import ch.wsl.box.rest.logic.NewsLoader import ch.wsl.box.rest.metadata.{BoxFormMetadataFactory, FormMetadataFactory, StubMetadataFactory} import ch.wsl.box.rest.io.pdf.PDFExport @@ -198,6 +198,14 @@ class PrivateArea(implicit ec:ExecutionContext, sessionManager: SessionManager[B } } + def lookupData(implicit up:UserProfile) = path("lookup-data") { + get{ + complete { + up.db.run(Registry.box().actions("v_lookup_data").findSimple(JSONQuery.empty)) + } + } + } + val websocket = new WebsocketNotifications() val route = auth ~ @@ -226,6 +234,7 @@ class PrivateArea(implicit ec:ExecutionContext, sessionManager: SessionManager[B translations ~ websocket.route ~ preferences(up) ~ + lookupData ~ Admin(session).route } case None => invalidateSession(oneOff, usingCookies) { From 06a7644fb41622b77abcf19d668ec6650e7e1b12 Mon Sep 17 00:00:00 2001 From: Andrea Minetti Date: Mon, 15 Jun 2026 15:18:14 +0200 Subject: [PATCH 7/7] fullheight revert --- .../src/main/scala/ch/wsl/box/client/styles/GlobalStyles.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 13d43c72..db065929 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 @@ -849,7 +849,7 @@ class GlobalStyles(settings:Settings,conf:StyleConf) extends StyleSheet.Inline() height :=! "calc(100vh - 53px)", overflow.auto, ), - overflow.hidden, + overflow.auto, width(100.%%) )