Add direct edit for work groups and rooms

This commit is contained in:
Lars Westermann 2019-05-18 18:53:46 +02:00
parent d08caf7b8b
commit e88db9c75c
Signed by: lars.westermann
GPG key ID: 9D417FA5BB9D5E1D
11 changed files with 468 additions and 73 deletions

View file

@ -1,8 +1,9 @@
package de.kif.frontend.views package de.kif.frontend.views
import de.kif.common.Search
import de.kif.common.SearchElement
import de.kif.frontend.iterator import de.kif.frontend.iterator
import de.kif.frontend.views.table.RoomTableLine
import de.kif.frontend.views.table.TableLine
import de.kif.frontend.views.table.WorkGroupTableLine
import de.westermann.kwebview.components.InputView import de.westermann.kwebview.components.InputView
import org.w3c.dom.HTMLFormElement import org.w3c.dom.HTMLFormElement
import org.w3c.dom.HTMLInputElement import org.w3c.dom.HTMLInputElement
@ -17,16 +18,20 @@ fun initTableLayout() {
val table = document.getElementsByClassName("table-layout-table")[0] as HTMLTableElement val table = document.getElementsByClassName("table-layout-table")[0] as HTMLTableElement
val list = table.getElementsByTagName("tr").iterator().asSequence().filter { val list = table.getElementsByTagName("tr").iterator().asSequence().filter {
it.dataset.get("search") != null it.dataset["search"] != null
}.associateWith { }.map {
SearchElement.parse(it.dataset.get("search")!!) when (it.dataset["edit"]) {
} "workgroup" -> WorkGroupTableLine(it)
"room" -> RoomTableLine(it)
else -> TableLine(it)
}
}.toList()
val input = form.getElementsByTagName("input")[0] as HTMLInputElement val input = form.getElementsByTagName("input")[0] as HTMLInputElement
val search = InputView.wrap(input) val search = InputView.wrap(input)
search.valueProperty.onChange { search.valueProperty.onChange {
for ((row, s) in list) { for (row in list) {
row.style.display = if (Search.match(search.value, s)) "table-row" else "none" row.search(search.value)
} }
} }
} }

View file

@ -0,0 +1,77 @@
package de.kif.frontend.views.table
import de.kif.common.SearchElement
import de.kif.frontend.iterator
import de.kif.frontend.launch
import de.kif.frontend.repository.RepositoryDelegate
import de.kif.frontend.repository.RoomRepository
import de.westermann.kwebview.components.TextView
import org.w3c.dom.HTMLElement
import org.w3c.dom.HTMLSpanElement
import org.w3c.dom.get
class RoomTableLine(view: HTMLElement) : TableLine(view) {
var lineId = dataset["id"]?.toLongOrNull() ?: -1
private val room =
RepositoryDelegate(RoomRepository, lineId)
private val spanRoomName: TextView
private val spanRoomPlaces: TextView
private val spanRoomProjector: TextView
override var searchElement: SearchElement = super.searchElement
init {
val spans = view.getElementsByTagName("span").iterator().asSequence().toList()
spanRoomName =
TextView.wrap(spans.first { it.dataset["editType"] == "room-name" } as HTMLSpanElement)
spanRoomPlaces =
TextView.wrap(spans.first { it.dataset["editType"] == "room-places" } as HTMLSpanElement)
spanRoomProjector =
TextView.wrap(spans.first { it.dataset["editType"] == "room-projector" } as HTMLSpanElement)
setupEditable(spanRoomName) {
launch {
val wg = room.get()
if (wg.name != it) {
RoomRepository.update(wg.copy(name = it))
}
}
}
setupEditable(spanRoomPlaces, "\\d+".toRegex()) {
val number = it.toIntOrNull() ?: return@setupEditable
launch {
val wg = room.get()
if (wg.places != number) {
RoomRepository.update(wg.copy(places = number))
}
}
}
setupBoolean(spanRoomProjector) {
launch {
val wg = room.get()
RoomRepository.update(wg.copy(projector = !wg.projector))
}
}
RoomRepository.onUpdate {
if (it != lineId) return@onUpdate
launch {
val wg = RoomRepository.get(it) ?: return@launch
room.set(wg)
searchElement = wg.createSearch()
spanRoomName.text = wg.name
spanRoomPlaces.text = wg.places.toString()
spanRoomProjector.text = wg.projector.toString()
}
}
}
}

View file

@ -0,0 +1,79 @@
package de.kif.frontend.views.table
import de.kif.common.Search
import de.kif.common.SearchElement
import de.westermann.kwebview.View
import de.westermann.kwebview.components.ListView
import de.westermann.kwebview.components.TextView
import de.westermann.kwebview.components.textView
import org.w3c.dom.HTMLElement
open class TableLine(line: HTMLElement) : View(line) {
open val searchElement: SearchElement = SearchElement.parse(dataset["search"]!!)
fun search(value: String) {
style.display = if (Search.match(value, searchElement)) "table-row" else "none"
}
protected fun setupEditable(view: TextView, regex: Regex = ".*".toRegex(), onSave: (String) -> Unit) {
view.contentEditable = true
view.onKeyDown {
if (it.keyCode == 13) {
it.preventDefault()
view.blur()
return@onKeyDown
}
}
view.onKeyUp {
view.classList["error"] = !regex.matches(view.text)
}
view.onBlur {
view.classList["error"] = !regex.matches(view.text)
if (!view.classList["error"]) {
onSave(view.text)
}
}
}
protected fun setupBoolean(view: TextView, onSave: () -> Unit) {
view.classList += "no-select"
view.tabIndex = 0
view.onDblClick {
onSave()
}
view.onKeyDown {
if (it.keyCode != 32) return@onKeyDown
onSave()
}
}
protected fun <T : Any> setupList(view: TextView, list: List<T?>, transform: (T) -> String, onSave: (T?) -> Unit) {
view.classList += "no-select"
view.tabIndex = 0
val listView = ListView<TextView>()
listView.classList += "table-select-box"
for (elem in list) {
val text = if (elem == null) "" else transform(elem)
listView.textView(text) {
onClick {
onSave(elem)
view.blur()
}
}
}
view.onFocus {
view.html.appendChild(listView.html)
}
view.onBlur {
listView.html.remove()
}
}
}

View file

@ -0,0 +1,133 @@
package de.kif.frontend.views.table
import de.kif.common.SearchElement
import de.kif.common.model.Language
import de.kif.common.model.Track
import de.kif.frontend.iterator
import de.kif.frontend.launch
import de.kif.frontend.repository.RepositoryDelegate
import de.kif.frontend.repository.TrackRepository
import de.kif.frontend.repository.WorkGroupRepository
import de.westermann.kwebview.components.TextView
import org.w3c.dom.HTMLElement
import org.w3c.dom.HTMLSpanElement
import org.w3c.dom.get
class WorkGroupTableLine(view: HTMLElement) : TableLine(view) {
var lineId = dataset["id"]?.toLongOrNull() ?: -1
private val workGroup =
RepositoryDelegate(WorkGroupRepository, lineId)
private val spanWorkGroupName: TextView
private val spanWorkGroupLength: TextView
private val spanWorkGroupInterested: TextView
private val spanWorkGroupTrack: TextView
private val spanWorkGroupProjector: TextView
private val spanWorkGroupResolution: TextView
private val spanWorkGroupLanguage: TextView
override var searchElement: SearchElement = super.searchElement
init {
val spans = view.getElementsByTagName("span").iterator().asSequence().toList()
spanWorkGroupName =
TextView.wrap(spans.first { it.dataset["editType"] == "workgroup-name" } as HTMLSpanElement)
spanWorkGroupLength =
TextView.wrap(spans.first { it.dataset["editType"] == "workgroup-length" } as HTMLSpanElement)
spanWorkGroupInterested =
TextView.wrap(spans.first { it.dataset["editType"] == "workgroup-interested" } as HTMLSpanElement)
spanWorkGroupTrack =
TextView.wrap(spans.first { it.dataset["editType"] == "workgroup-track" } as HTMLSpanElement)
spanWorkGroupProjector =
TextView.wrap(spans.first { it.dataset["editType"] == "workgroup-projector" } as HTMLSpanElement)
spanWorkGroupResolution =
TextView.wrap(spans.first { it.dataset["editType"] == "workgroup-resolution" } as HTMLSpanElement)
spanWorkGroupLanguage =
TextView.wrap(spans.first { it.dataset["editType"] == "workgroup-language" } as HTMLSpanElement)
setupEditable(spanWorkGroupName) {
launch {
val wg = workGroup.get()
if (wg.name != it) {
WorkGroupRepository.update(wg.copy(name = it))
}
}
}
setupEditable(spanWorkGroupLength, "\\d+".toRegex()) {
val number = it.toIntOrNull() ?: return@setupEditable
launch {
val wg = workGroup.get()
if (wg.length != number) {
WorkGroupRepository.update(wg.copy(length = number))
}
}
}
setupEditable(spanWorkGroupInterested, "\\d+".toRegex()) {
val number = it.toIntOrNull() ?: return@setupEditable
launch {
val wg = workGroup.get()
if (wg.interested != number) {
WorkGroupRepository.update(wg.copy(interested = number))
}
}
}
setupBoolean(spanWorkGroupProjector) {
launch {
val wg = workGroup.get()
WorkGroupRepository.update(wg.copy(projector = !wg.projector))
}
}
setupBoolean(spanWorkGroupResolution) {
launch {
val wg = workGroup.get()
WorkGroupRepository.update(wg.copy(resolution = !wg.resolution))
}
}
setupList(spanWorkGroupLanguage, Language.values().sortedBy { it.localeName }, { it.localeName }) {
if (it == null) return@setupList
launch {
val wg = workGroup.get()
if (wg.language == it) return@launch
WorkGroupRepository.update(wg.copy(language = it))
}
}
launch {
val tracks = listOf<Track?>(null) + TrackRepository.all()
setupList(spanWorkGroupTrack, tracks, { it.name }) {
launch x@{
val wg = workGroup.get()
if (wg.track == it) return@x
WorkGroupRepository.update(wg.copy(track = it))
}
}
}
WorkGroupRepository.onUpdate {
if (it != lineId) return@onUpdate
launch {
val wg = WorkGroupRepository.get(it) ?: return@launch
workGroup.set(wg)
searchElement = wg.createSearch()
spanWorkGroupName.text = wg.name
spanWorkGroupLength.text = wg.length.toString()
spanWorkGroupInterested.text = wg.interested.toString()
spanWorkGroupTrack.text = wg.track?.name ?: ""
spanWorkGroupProjector.text = wg.projector.toString()
spanWorkGroupResolution.text = wg.resolution.toString()
spanWorkGroupLanguage.text = wg.language.localeName
}
}
}
}

View file

@ -3,10 +3,7 @@ package de.westermann.kwebview.components
import de.westermann.kobserve.Property import de.westermann.kobserve.Property
import de.westermann.kobserve.ReadOnlyProperty import de.westermann.kobserve.ReadOnlyProperty
import de.westermann.kobserve.property.property import de.westermann.kobserve.property.property
import de.westermann.kwebview.KWebViewDsl import de.westermann.kwebview.*
import de.westermann.kwebview.View
import de.westermann.kwebview.ViewCollection
import de.westermann.kwebview.createHtmlView
import org.w3c.dom.HTMLSpanElement import org.w3c.dom.HTMLSpanElement
/** /**
@ -15,8 +12,9 @@ import org.w3c.dom.HTMLSpanElement
* @author lars * @author lars
*/ */
class TextView( class TextView(
value: String = "" value: String = "",
) : View(createHtmlView<HTMLSpanElement>()) { view: HTMLSpanElement = createHtmlView()
) : View(view) {
override val html = super.html as HTMLSpanElement override val html = super.html as HTMLSpanElement
@ -37,9 +35,26 @@ class TextView(
val textProperty: Property<String> = property(this::text) val textProperty: Property<String> = property(this::text)
var contentEditable: Boolean
get() = html.isContentEditable
set(value) {
html.contentEditable = value.toString()
}
private var internalTabIndex by AttributeDelegate("tabIndex")
var tabIndex: Int?
get() = internalTabIndex?.toIntOrNull()
set(value) {
internalTabIndex = value?.toString()
}
init { init {
text = value text = value
} }
companion object {
fun wrap(view: HTMLSpanElement) = TextView(view.textContent ?: "", view)
}
} }
@KWebViewDsl @KWebViewDsl

View file

@ -27,6 +27,8 @@ $bg-enabled-color: rgba($primary-color, .5);
$lever-disabled-color: $background-primary-color; $lever-disabled-color: $background-primary-color;
$lever-enabled-color: $primary-color; $lever-enabled-color: $primary-color;
$error-background-color: #FFCDD2;
body, html { body, html {
color: $text-primary-color; color: $text-primary-color;
background: $background-secondary-color; background: $background-secondary-color;
@ -40,6 +42,10 @@ body, html {
padding: 0; padding: 0;
} }
.no-select {
@include no-select()
}
a { a {
text-decoration: none; text-decoration: none;
outline: none; outline: none;
@ -248,6 +254,10 @@ a {
tr { tr {
border-top: solid 1px rgba($text-primary-color, 0.1); border-top: solid 1px rgba($text-primary-color, 0.1);
&:nth-child(odd) {
//background-color: rgba($text-primary-color, 0.01);
}
&:first-child { &:first-child {
background-color: rgba($text-primary-color, 0.06); background-color: rgba($text-primary-color, 0.06);
height: 2.5rem; height: 2.5rem;
@ -263,6 +273,34 @@ a {
} }
} }
} }
span {
display: block;
position: relative;
&:empty:before {
content: "\200b";
}
&.error {
background-color: $error-background-color;
}
}
.table-select-box {
position: absolute;
z-index: 1;
background: $background-primary-color;
width: 100%;
border: solid 1px rgba($text-primary-color, 0.1);
span {
padding: 0 0.5rem;
&:hover {
background-color: rgba($text-primary-color, 0.06);
}
}
}
} }
.form-control { .form-control {

View file

@ -2,10 +2,10 @@ package de.kif.backend.route
import de.kif.backend.authenticateOrRedirect import de.kif.backend.authenticateOrRedirect
import de.kif.backend.repository.RoomRepository import de.kif.backend.repository.RoomRepository
import de.kif.common.Search
import de.kif.backend.view.MainTemplate import de.kif.backend.view.MainTemplate
import de.kif.backend.view.MenuTemplate import de.kif.backend.view.MenuTemplate
import de.kif.backend.view.TableTemplate import de.kif.backend.view.TableTemplate
import de.kif.common.Search
import de.kif.common.model.Permission import de.kif.common.model.Permission
import de.kif.common.model.Room import de.kif.common.model.Room
import io.ktor.application.call import io.ktor.application.call
@ -17,11 +17,11 @@ import io.ktor.routing.Route
import io.ktor.routing.get import io.ktor.routing.get
import io.ktor.routing.post import io.ktor.routing.post
import io.ktor.util.toMap import io.ktor.util.toMap
import kotlinx.css.CSSBuilder
import kotlinx.css.Display
import kotlinx.html.* import kotlinx.html.*
import kotlin.collections.component1 import kotlin.collections.component1
import kotlin.collections.component2 import kotlin.collections.component2
import kotlin.collections.firstOrNull
import kotlin.collections.mapValues
import kotlin.collections.set import kotlin.collections.set
fun Route.room() { fun Route.room() {
@ -65,22 +65,38 @@ fun Route.room() {
for (u in list) { for (u in list) {
val s = u.createSearch() val s = u.createSearch()
if (Search.match(search, s)) { entry {
entry { attributes["style"] = CSSBuilder().apply {
attributes["data-search"] = s.stringify() display = if (Search.match(search, s)) Display.tableRow else Display.none
td { }.toString()
attributes["data-search"] = s.stringify()
attributes["data-edit"] = "room"
attributes["data-id"] = u.id.toString()
td {
span {
attributes["data-edit-type"] = "room-name"
+u.name +u.name
} }
td { }
td {
span {
attributes["data-edit-type"] = "room-places"
+u.places.toString() +u.places.toString()
} }
td { }
td {
span {
attributes["data-edit-type"] = "room-projector"
+u.projector.toString() +u.projector.toString()
} }
td(classes = "action") { }
a("/room/${u.id}") { td(classes = "action") {
i("material-icons") { +"edit" } a("/room/${u.id}") {
} i("material-icons") { +"edit" }
} }
} }
} }

View file

@ -3,10 +3,10 @@ package de.kif.backend.route
import de.kif.backend.authenticateOrRedirect import de.kif.backend.authenticateOrRedirect
import de.kif.backend.repository.TrackRepository import de.kif.backend.repository.TrackRepository
import de.kif.common.Search
import de.kif.backend.view.MainTemplate import de.kif.backend.view.MainTemplate
import de.kif.backend.view.MenuTemplate import de.kif.backend.view.MenuTemplate
import de.kif.backend.view.TableTemplate import de.kif.backend.view.TableTemplate
import de.kif.common.Search
import de.kif.common.model.Color import de.kif.common.model.Color
import de.kif.common.model.Permission import de.kif.common.model.Permission
import de.kif.common.model.Track import de.kif.common.model.Track
@ -20,6 +20,7 @@ import io.ktor.routing.get
import io.ktor.routing.post import io.ktor.routing.post
import io.ktor.util.toMap import io.ktor.util.toMap
import kotlinx.css.CSSBuilder import kotlinx.css.CSSBuilder
import kotlinx.css.Display
import kotlinx.html.* import kotlinx.html.*
import kotlin.collections.set import kotlin.collections.set
import kotlin.random.Random import kotlin.random.Random
@ -119,22 +120,24 @@ fun Route.track() {
for (u in list) { for (u in list) {
val s = u.createSearch() val s = u.createSearch()
if (Search.match(search, s)) { entry {
entry { attributes["style"] = CSSBuilder().apply {
attributes["data-search"] = s.stringify() display = if (Search.match(search, s)) Display.tableRow else Display.none
td { }.toString()
+u.name attributes["data-search"] = s.stringify()
} td {
td { +u.name
+u.color.toString() }
} td {
td(classes = "action") { +u.color.toString()
a("/track/${u.id}") { }
i("material-icons") { +"edit" } td(classes = "action") {
} a("/track/${u.id}") {
i("material-icons") { +"edit" }
} }
} }
} }
} }
} }
} }

View file

@ -4,10 +4,10 @@ package de.kif.backend.route
import de.kif.backend.authenticateOrRedirect import de.kif.backend.authenticateOrRedirect
import de.kif.backend.hashPassword import de.kif.backend.hashPassword
import de.kif.backend.repository.UserRepository import de.kif.backend.repository.UserRepository
import de.kif.common.Search
import de.kif.backend.view.MainTemplate import de.kif.backend.view.MainTemplate
import de.kif.backend.view.MenuTemplate import de.kif.backend.view.MenuTemplate
import de.kif.backend.view.TableTemplate import de.kif.backend.view.TableTemplate
import de.kif.common.Search
import de.kif.common.model.Permission import de.kif.common.model.Permission
import de.kif.common.model.User import de.kif.common.model.User
import io.ktor.application.call import io.ktor.application.call
@ -19,16 +19,12 @@ import io.ktor.routing.Route
import io.ktor.routing.get import io.ktor.routing.get
import io.ktor.routing.post import io.ktor.routing.post
import io.ktor.util.toMap import io.ktor.util.toMap
import kotlinx.css.CSSBuilder
import kotlinx.css.Display
import kotlinx.html.* import kotlinx.html.*
import kotlin.collections.component1 import kotlin.collections.component1
import kotlin.collections.component2 import kotlin.collections.component2
import kotlin.collections.filter
import kotlin.collections.firstOrNull
import kotlin.collections.joinToString
import kotlin.collections.mapNotNull
import kotlin.collections.mapValues
import kotlin.collections.set import kotlin.collections.set
import kotlin.collections.toSet
fun Route.user() { fun Route.user() {
get("/users") { param -> get("/users") { param ->
@ -67,19 +63,20 @@ fun Route.user() {
for (u in list) { for (u in list) {
val s = u.createSearch() val s = u.createSearch()
if (Search.match(search, s)) { entry {
entry { attributes["style"] = CSSBuilder().apply {
attributes["data-search"] = s.stringify() display = if (Search.match(search, s)) Display.tableRow else Display.none
td { }.toString()
+u.username attributes["data-search"] = s.stringify()
} td {
td { +u.username
+u.permissions.joinToString(", ") { it.toString().toLowerCase() } }
} td {
td(classes = "action") { +u.permissions.joinToString(", ") { it.toString().toLowerCase() }
a("/user/${u.id}") { }
i("material-icons") { +"edit" } td(classes = "action") {
} a("/user/${u.id}") {
i("material-icons") { +"edit" }
} }
} }
} }

View file

@ -57,6 +57,9 @@ fun Route.workGroup() {
th { th {
+"Name" +"Name"
} }
th {
+"Length"
}
th { th {
+"Interested" +"Interested"
} }
@ -69,9 +72,6 @@ fun Route.workGroup() {
th { th {
+"Resolution" +"Resolution"
} }
th {
+"Length"
}
th { th {
+"Language" +"Language"
} }
@ -87,26 +87,57 @@ fun Route.workGroup() {
display = if (Search.match(search, s)) Display.tableRow else Display.none display = if (Search.match(search, s)) Display.tableRow else Display.none
}.toString() }.toString()
attributes["data-search"] = s.stringify() attributes["data-search"] = s.stringify()
attributes["data-edit"] = "workgroup"
attributes["data-id"] = u.id.toString()
td { td {
+u.name span {
attributes["data-edit-type"] = "workgroup-name"
+u.name
}
} }
td { td {
+u.interested.toString() span {
attributes["data-edit-type"] = "workgroup-length"
+u.length.toString()
}
} }
td { td {
+(u.track?.name ?: "") span {
attributes["data-edit-type"] = "workgroup-interested"
+u.interested.toString()
}
} }
td { td {
+u.projector.toString() span {
attributes["data-edit-type"] = "workgroup-track"
+(u.track?.name ?: "")
}
} }
td { td {
+u.resolution.toString() span {
attributes["data-edit-type"] = "workgroup-projector"
+u.projector.toString()
}
} }
td { td {
+u.length.toString() span {
attributes["data-edit-type"] = "workgroup-resolution"
+u.resolution.toString()
}
} }
td { td {
+u.language.localeName span {
attributes["data-edit-type"] = "workgroup-language"
+u.language.localeName
}
} }
td(classes = "action") { td(classes = "action") {
a("/workgroup/${u.id}") { a("/workgroup/${u.id}") {

View file

@ -4,7 +4,7 @@ import io.ktor.html.*
import kotlinx.html.* import kotlinx.html.*
class TableTemplate() : Template<FlowContent> { class TableTemplate(private val classes: String = "") : Template<FlowContent> {
var searchValue = "" var searchValue = ""
@ -13,7 +13,8 @@ class TableTemplate() : Template<FlowContent> {
val entry = PlaceholderList<TABLE, TR>() val entry = PlaceholderList<TABLE, TR>()
override fun FlowContent.apply() { override fun FlowContent.apply() {
div("table-layout") { val c = "table-layout" + if (classes.isEmpty()) "" else " $classes"
div(c) {
form(classes = "form-group table-layout-search") { form(classes = "form-group table-layout-search") {
div("input-group") { div("input-group") {
input(InputType.search, name = "search", classes = "form-control") { input(InputType.search, name = "search", classes = "form-control") {