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
import de.kif.common.Search
import de.kif.common.SearchElement
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 org.w3c.dom.HTMLFormElement
import org.w3c.dom.HTMLInputElement
@ -17,16 +18,20 @@ fun initTableLayout() {
val table = document.getElementsByClassName("table-layout-table")[0] as HTMLTableElement
val list = table.getElementsByTagName("tr").iterator().asSequence().filter {
it.dataset.get("search") != null
}.associateWith {
SearchElement.parse(it.dataset.get("search")!!)
}
it.dataset["search"] != null
}.map {
when (it.dataset["edit"]) {
"workgroup" -> WorkGroupTableLine(it)
"room" -> RoomTableLine(it)
else -> TableLine(it)
}
}.toList()
val input = form.getElementsByTagName("input")[0] as HTMLInputElement
val search = InputView.wrap(input)
search.valueProperty.onChange {
for ((row, s) in list) {
row.style.display = if (Search.match(search.value, s)) "table-row" else "none"
for (row in list) {
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.ReadOnlyProperty
import de.westermann.kobserve.property.property
import de.westermann.kwebview.KWebViewDsl
import de.westermann.kwebview.View
import de.westermann.kwebview.ViewCollection
import de.westermann.kwebview.createHtmlView
import de.westermann.kwebview.*
import org.w3c.dom.HTMLSpanElement
/**
@ -15,8 +12,9 @@ import org.w3c.dom.HTMLSpanElement
* @author lars
*/
class TextView(
value: String = ""
) : View(createHtmlView<HTMLSpanElement>()) {
value: String = "",
view: HTMLSpanElement = createHtmlView()
) : View(view) {
override val html = super.html as HTMLSpanElement
@ -37,9 +35,26 @@ class TextView(
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 {
text = value
}
companion object {
fun wrap(view: HTMLSpanElement) = TextView(view.textContent ?: "", view)
}
}
@KWebViewDsl

View file

@ -27,6 +27,8 @@ $bg-enabled-color: rgba($primary-color, .5);
$lever-disabled-color: $background-primary-color;
$lever-enabled-color: $primary-color;
$error-background-color: #FFCDD2;
body, html {
color: $text-primary-color;
background: $background-secondary-color;
@ -40,6 +42,10 @@ body, html {
padding: 0;
}
.no-select {
@include no-select()
}
a {
text-decoration: none;
outline: none;
@ -248,6 +254,10 @@ a {
tr {
border-top: solid 1px rgba($text-primary-color, 0.1);
&:nth-child(odd) {
//background-color: rgba($text-primary-color, 0.01);
}
&:first-child {
background-color: rgba($text-primary-color, 0.06);
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 {

View file

@ -2,10 +2,10 @@ package de.kif.backend.route
import de.kif.backend.authenticateOrRedirect
import de.kif.backend.repository.RoomRepository
import de.kif.common.Search
import de.kif.backend.view.MainTemplate
import de.kif.backend.view.MenuTemplate
import de.kif.backend.view.TableTemplate
import de.kif.common.Search
import de.kif.common.model.Permission
import de.kif.common.model.Room
import io.ktor.application.call
@ -17,11 +17,11 @@ import io.ktor.routing.Route
import io.ktor.routing.get
import io.ktor.routing.post
import io.ktor.util.toMap
import kotlinx.css.CSSBuilder
import kotlinx.css.Display
import kotlinx.html.*
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.collections.firstOrNull
import kotlin.collections.mapValues
import kotlin.collections.set
fun Route.room() {
@ -65,22 +65,38 @@ fun Route.room() {
for (u in list) {
val s = u.createSearch()
if (Search.match(search, s)) {
entry {
attributes["data-search"] = s.stringify()
td {
entry {
attributes["style"] = CSSBuilder().apply {
display = if (Search.match(search, s)) Display.tableRow else Display.none
}.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
}
td {
}
td {
span {
attributes["data-edit-type"] = "room-places"
+u.places.toString()
}
td {
}
td {
span {
attributes["data-edit-type"] = "room-projector"
+u.projector.toString()
}
td(classes = "action") {
a("/room/${u.id}") {
i("material-icons") { +"edit" }
}
}
td(classes = "action") {
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.repository.TrackRepository
import de.kif.common.Search
import de.kif.backend.view.MainTemplate
import de.kif.backend.view.MenuTemplate
import de.kif.backend.view.TableTemplate
import de.kif.common.Search
import de.kif.common.model.Color
import de.kif.common.model.Permission
import de.kif.common.model.Track
@ -20,6 +20,7 @@ import io.ktor.routing.get
import io.ktor.routing.post
import io.ktor.util.toMap
import kotlinx.css.CSSBuilder
import kotlinx.css.Display
import kotlinx.html.*
import kotlin.collections.set
import kotlin.random.Random
@ -119,22 +120,24 @@ fun Route.track() {
for (u in list) {
val s = u.createSearch()
if (Search.match(search, s)) {
entry {
attributes["data-search"] = s.stringify()
td {
+u.name
}
td {
+u.color.toString()
}
td(classes = "action") {
a("/track/${u.id}") {
i("material-icons") { +"edit" }
}
entry {
attributes["style"] = CSSBuilder().apply {
display = if (Search.match(search, s)) Display.tableRow else Display.none
}.toString()
attributes["data-search"] = s.stringify()
td {
+u.name
}
td {
+u.color.toString()
}
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.hashPassword
import de.kif.backend.repository.UserRepository
import de.kif.common.Search
import de.kif.backend.view.MainTemplate
import de.kif.backend.view.MenuTemplate
import de.kif.backend.view.TableTemplate
import de.kif.common.Search
import de.kif.common.model.Permission
import de.kif.common.model.User
import io.ktor.application.call
@ -19,16 +19,12 @@ import io.ktor.routing.Route
import io.ktor.routing.get
import io.ktor.routing.post
import io.ktor.util.toMap
import kotlinx.css.CSSBuilder
import kotlinx.css.Display
import kotlinx.html.*
import kotlin.collections.component1
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.toSet
fun Route.user() {
get("/users") { param ->
@ -67,19 +63,20 @@ fun Route.user() {
for (u in list) {
val s = u.createSearch()
if (Search.match(search, s)) {
entry {
attributes["data-search"] = s.stringify()
td {
+u.username
}
td {
+u.permissions.joinToString(", ") { it.toString().toLowerCase() }
}
td(classes = "action") {
a("/user/${u.id}") {
i("material-icons") { +"edit" }
}
entry {
attributes["style"] = CSSBuilder().apply {
display = if (Search.match(search, s)) Display.tableRow else Display.none
}.toString()
attributes["data-search"] = s.stringify()
td {
+u.username
}
td {
+u.permissions.joinToString(", ") { it.toString().toLowerCase() }
}
td(classes = "action") {
a("/user/${u.id}") {
i("material-icons") { +"edit" }
}
}
}

View file

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

View file

@ -4,7 +4,7 @@ import io.ktor.html.*
import kotlinx.html.*
class TableTemplate() : Template<FlowContent> {
class TableTemplate(private val classes: String = "") : Template<FlowContent> {
var searchValue = ""
@ -13,7 +13,8 @@ class TableTemplate() : Template<FlowContent> {
val entry = PlaceholderList<TABLE, TR>()
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") {
div("input-group") {
input(InputType.search, name = "search", classes = "form-control") {