Add drag and drop creation of schedule

This commit is contained in:
Lars Westermann 2019-05-17 22:39:42 +02:00
parent b7d6476a70
commit 84f8e71239
Signed by: lars.westermann
GPG key ID: 9D417FA5BB9D5E1D
38 changed files with 1277 additions and 548 deletions

View file

@ -29,6 +29,7 @@ repositories {
} }
def ktor_version = '1.1.5' def ktor_version = '1.1.5'
def serialization_version = '0.11.0' def serialization_version = '0.11.0'
def observable_version = '0.9.3'
kotlin { kotlin {
jvm() { jvm() {
@ -53,7 +54,7 @@ kotlin {
commonMain { commonMain {
dependencies { dependencies {
implementation kotlin('stdlib-common') implementation kotlin('stdlib-common')
implementation "de.westermann:KObserve-metadata:0.9.1" implementation "de.westermann:KObserve-metadata:$observable_version"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-common:$serialization_version" implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-common:$serialization_version"
} }
@ -84,7 +85,7 @@ kotlin {
implementation 'org.mindrot:jbcrypt:0.4' implementation 'org.mindrot:jbcrypt:0.4'
implementation "de.westermann:KObserve-jvm:0.9.1" implementation "de.westermann:KObserve-jvm:$observable_version"
api 'io.github.microutils:kotlin-logging:1.6.23' api 'io.github.microutils:kotlin-logging:1.6.23'
api 'ch.qos.logback:logback-classic:1.2.3' api 'ch.qos.logback:logback-classic:1.2.3'
@ -103,7 +104,7 @@ kotlin {
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-js:$serialization_version" implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-js:$serialization_version"
implementation "de.westermann:KObserve-js:0.9.1" implementation "de.westermann:KObserve-js:$observable_version"
} }
} }
jsTest { jsTest {

View file

@ -0,0 +1,120 @@
package de.kif.common
import kotlinx.serialization.Serializable
@Serializable
data class SearchElement(
val fields: Map<String, String> = emptyMap(),
val flags: Map<String, Boolean> = emptyMap(),
val numbers: Map<String, Double> = emptyMap()
) {
fun stringify(): String {
return Message.json.stringify(serializer(), this)
}
companion object {
fun parse(data: String): SearchElement {
return Message.json.parse(serializer(), data)
}
}
}
object Search {
private val regex = """
((\w+)\s?[:=]\s?)?
((\[.*]|\+\w+|-\w+|!\w+)|
((\w+)\s?([<=>]+)\s?(\d+))|
(\w+|"(.*)"))
""".trimIndent().replace("\n", "").toRegex()
fun match(search: String, element: SearchElement): Boolean {
val matches = regex.findAll(search)
val fields = mutableMapOf<String, String>()
val flags = mutableMapOf<String, Boolean>()
val numbers = mutableMapOf<String, ClosedRange<Double>>()
for (match in matches) {
val name = match.groups[2]?.value ?: ""
val field = match.groups[10]?.value ?: match.groups[9]?.value
val flag = match.groups[4]?.value
val numberName = match.groups[6]?.value
val numberRelation = match.groups[7]?.value
val numberDigits = match.groups[8]?.value?.toDoubleOrNull()
if (flag != null) {
val h = flag.replace("[\\[\\]+!\\-]".toRegex(), "")
val b = ("-" !in flag && "!" !in flag)
flags[h] = b
} else if (numberName != null && numberRelation != null && numberDigits != null) {
when (numberRelation) {
"<" -> numbers[numberName] = Double.NEGATIVE_INFINITY..(numberDigits - Double.MIN_VALUE)
"<=" -> numbers[numberName] = Double.NEGATIVE_INFINITY..numberDigits
"==" -> numbers[numberName] = numberDigits..numberDigits
">" -> numbers[numberName] = (numberDigits + Double.MIN_VALUE)..Double.POSITIVE_INFINITY
">=" -> numbers[numberName] = numberDigits..Double.POSITIVE_INFINITY
}
} else if (field != null) {
val old = fields[name]
fields[name] = if (old == null) field else "$old $field"
}
}
val fieldRadio = if (fields.isEmpty()) 1.0 else fields.count { (searchKey, searchValue) ->
for ((elementKey, elementValue) in element.fields) {
if (elementKey.contains(searchKey, true) && elementValue.contains(searchValue, true)) {
return@count true
}
}
for ((elementKey, elementValue) in element.flags) {
if (elementValue && elementKey.contains(searchValue, true)) {
return@count true
}
}
for ((elementKey, _) in element.numbers) {
if (elementKey.contains(searchValue, true)) {
return@count true
}
}
false
} / fields.size.toDouble()
for ((searchKey, searchValue) in flags) {
for ((elementKey, elementValue) in element.flags) {
if (elementKey.contains(searchKey, true) && searchValue != elementValue)
return false
}
}
for ((searchKey, searchValue) in numbers) {
for ((elementKey, elementValue) in element.numbers) {
if (elementKey.contains(searchKey, true) && elementValue !in searchValue)
return false
}
}
//println("$fieldRadio (${fieldRadio >= 0.5}) for $element")
return fieldRadio >= 0.5
}
}
/*
fun main() {
val element = SearchElement(
mapOf(
"name" to "lorem"
), mapOf(
"beamer" to true,
"room" to true,
"day" to false
), mapOf(
"places" to 500.0
)
)
println(Search.match("""search text data:"text to search" name = hans""", element))
println(Search.match("""lorem [beamer] places >= 100 +room -day""", element))
}
*/

View file

@ -1,3 +1,7 @@
package de.kif.common.model package de.kif.common.model
interface Model import de.kif.common.SearchElement
interface Model {
fun createSearch(): SearchElement
}

View file

@ -1,5 +1,6 @@
package de.kif.common.model package de.kif.common.model
import de.kif.common.SearchElement
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
@ -8,4 +9,15 @@ data class Room(
val name: String, val name: String,
val places: Int, val places: Int,
val projector: Boolean val projector: Boolean
) : Model ) : Model {
override fun createSearch() = SearchElement(
mapOf(
"name" to name
), mapOf(
"projector" to projector
), mapOf(
"places" to places.toDouble()
)
)
}

View file

@ -1,5 +1,6 @@
package de.kif.common.model package de.kif.common.model
import de.kif.common.SearchElement
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
@ -9,4 +10,16 @@ data class Schedule(
val room: Room, val room: Room,
val day: Int, val day: Int,
val time: Int val time: Int
) : Model ) : Model {
override fun createSearch() = SearchElement(
mapOf(
"workgroup" to workGroup.name,
"room" to room.name,
"track" to (workGroup.track?.name ?: ""),
"language" to workGroup.language.localeName
), mapOf(), mapOf(
"day" to day.toDouble()
)
)
}

View file

@ -1,10 +1,18 @@
package de.kif.common.model package de.kif.common.model
import de.kif.common.SearchElement
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
data class Track( data class Track(
val id: Long?, val id: Long?,
var name: String, val name: String,
var color: Color val color: Color
) : Model ) : Model {
override fun createSearch() = SearchElement(
mapOf(
"name" to name
)
)
}

View file

@ -1,5 +1,6 @@
package de.kif.common.model package de.kif.common.model
import de.kif.common.SearchElement
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
@ -13,4 +14,10 @@ data class User(
fun checkPermission(permission: Permission): Boolean { fun checkPermission(permission: Permission): Boolean {
return permission in permissions || Permission.ADMIN in permissions return permission in permissions || Permission.ADMIN in permissions
} }
override fun createSearch() = SearchElement(
mapOf(
"username" to username
)
)
} }

View file

@ -1,5 +1,6 @@
package de.kif.common.model package de.kif.common.model
import de.kif.common.SearchElement
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
@ -12,4 +13,19 @@ data class WorkGroup(
val resolution: Boolean, val resolution: Boolean,
val length: Int, val length: Int,
val language: Language val language: Language
) : Model ) : Model {
override fun createSearch() = SearchElement(
mapOf(
"name" to name,
"track" to (track?.name ?: ""),
"language" to language.localeName
), mapOf(
"projector" to projector,
"resolution" to resolution
), mapOf(
"interested" to interested.toDouble(),
"length" to length.toDouble()
)
)
}

View file

@ -1,6 +1,7 @@
package de.kif.frontend package de.kif.frontend
import de.kif.frontend.views.initCalendar import de.kif.frontend.views.calendar.initCalendar
import de.kif.frontend.views.initTableLayout
import de.westermann.kwebview.components.init import de.westermann.kwebview.components.init
import kotlin.browser.document import kotlin.browser.document
@ -10,4 +11,7 @@ fun main() = init {
if (document.getElementsByClassName("calendar").length > 0) { if (document.getElementsByClassName("calendar").length > 0) {
initCalendar() initCalendar()
} }
if (document.getElementsByClassName("table-layout").length > 0) {
initTableLayout()
}
} }

View file

@ -23,7 +23,7 @@ object RoomRepository : Repository<Room> {
} }
override suspend fun create(model: Room): Long { override suspend fun create(model: Room): Long {
return repositoryPost("/api/rooms", Message.json.stringify(Room.serializer(), model))?.toLong() return repositoryPost("/api/rooms", Message.json.stringify(Room.serializer(), model))
?: throw IllegalStateException("Cannot create model!") ?: throw IllegalStateException("Cannot create model!")
} }

View file

@ -23,7 +23,7 @@ object ScheduleRepository : Repository<Schedule> {
} }
override suspend fun create(model: Schedule): Long { override suspend fun create(model: Schedule): Long {
return repositoryPost("/api/schedules", Message.json.stringify(Schedule.serializer(), model))?.toLong() return repositoryPost("/api/schedules", Message.json.stringify(Schedule.serializer(), model))
?: throw IllegalStateException("Cannot create model!") ?: throw IllegalStateException("Cannot create model!")
} }

View file

@ -23,7 +23,7 @@ object TrackRepository : Repository<Track> {
} }
override suspend fun create(model: Track): Long { override suspend fun create(model: Track): Long {
return repositoryPost("/api/tracks", Message.json.stringify(Track.serializer(), model))?.toLong() return repositoryPost("/api/tracks", Message.json.stringify(Track.serializer(), model))
?: throw IllegalStateException("Cannot create model!") ?: throw IllegalStateException("Cannot create model!")
} }

View file

@ -23,7 +23,7 @@ object UserRepository : Repository<User> {
} }
override suspend fun create(model: User): Long { override suspend fun create(model: User): Long {
return repositoryPost("/api/users", Message.json.stringify(User.serializer(), model))?.toLong() return repositoryPost("/api/users", Message.json.stringify(User.serializer(), model))
?: throw IllegalStateException("Cannot create model!") ?: throw IllegalStateException("Cannot create model!")
} }

View file

@ -23,7 +23,7 @@ object WorkGroupRepository : Repository<WorkGroup> {
} }
override suspend fun create(model: WorkGroup): Long { override suspend fun create(model: WorkGroup): Long {
return repositoryPost("/api/workgroups", Message.json.stringify(WorkGroup.serializer(), model))?.toLong() return repositoryPost("/api/workgroups", Message.json.stringify(WorkGroup.serializer(), model))
?: throw IllegalStateException("Cannot create model!") ?: throw IllegalStateException("Cannot create model!")
} }

View file

@ -1,359 +0,0 @@
package de.kif.frontend.views
import de.kif.common.CALENDAR_GRID_WIDTH
import de.kif.common.model.Room
import de.kif.common.model.Schedule
import de.kif.frontend.iterator
import de.kif.frontend.launch
import de.kif.frontend.repository.RepositoryDelegate
import de.kif.frontend.repository.RoomRepository
import de.kif.frontend.repository.ScheduleRepository
import de.westermann.kwebview.*
import de.westermann.kwebview.components.Body
import org.w3c.dom.HTMLAnchorElement
import org.w3c.dom.HTMLElement
import org.w3c.dom.events.EventListener
import org.w3c.dom.events.MouseEvent
import org.w3c.dom.get
import kotlin.browser.document
import kotlin.dom.appendText
import kotlin.dom.isText
class CalendarTools(entry: CalendarEntry, view: HTMLElement) : View(view) {
init {
var linkM10: HTMLAnchorElement? = null
var linkM5: HTMLAnchorElement? = null
var linkReset: HTMLAnchorElement? = null
var linkP5: HTMLAnchorElement? = null
var linkP10: HTMLAnchorElement? = null
var linkDel: HTMLAnchorElement? = null
for (element in html.children) {
when {
element.classList.contains("calendar-tools-m10") -> linkM10 = element as? HTMLAnchorElement
element.classList.contains("calendar-tools-m5") -> linkM5 = element as? HTMLAnchorElement
element.classList.contains("calendar-tools-reset") -> linkReset = element as? HTMLAnchorElement
element.classList.contains("calendar-tools-p5") -> linkP5 = element as? HTMLAnchorElement
element.classList.contains("calendar-tools-p10") -> linkP10 = element as? HTMLAnchorElement
element.classList.contains("calendar-tools-del") -> linkDel = element as? HTMLAnchorElement
}
}
linkM10 = linkM10 ?: run {
val link = createHtmlView<HTMLAnchorElement>()
link.classList.add("calendar-tools-m10")
link.textContent = "-10"
html.appendChild(link)
link
}
linkM10.removeAttribute("href")
linkM10.addEventListener("click", EventListener {
entry.pending = true
launch {
val s = entry.schedule.get()
ScheduleRepository.update(s.copy(time = s.time - 10))
}
})
linkM5 = linkM5 ?: run {
val link = createHtmlView<HTMLAnchorElement>()
link.classList.add("calendar-tools-m5")
link.textContent = "-5"
html.appendChild(link)
link
}
linkM5.removeAttribute("href")
linkM5.addEventListener("click", EventListener {
entry.pending = true
launch {
val s = entry.schedule.get()
ScheduleRepository.update(s.copy(time = s.time - 5))
}
})
linkReset = linkReset ?: run {
val link = createHtmlView<HTMLAnchorElement>()
link.classList.add("calendar-tools-reset")
link.textContent = "reset"
html.appendChild(link)
link
}
linkReset.removeAttribute("href")
linkReset.addEventListener("click", EventListener {
entry.pending = true
launch {
val s = entry.schedule.get()
ScheduleRepository.update(s.copy(time = s.time / CALENDAR_GRID_WIDTH * CALENDAR_GRID_WIDTH))
}
})
linkP5 = linkP5 ?: run {
val link = createHtmlView<HTMLAnchorElement>()
link.classList.add("calendar-tools-p5")
link.textContent = "+5"
html.appendChild(link)
link
}
linkP5.removeAttribute("href")
linkP5.addEventListener("click", EventListener {
entry.pending = true
launch {
val s = entry.schedule.get()
ScheduleRepository.update(s.copy(time = s.time + 5))
}
})
linkP10 = linkP10 ?: run {
val link = createHtmlView<HTMLAnchorElement>()
link.classList.add("calendar-tools-p10")
link.textContent = "+10"
html.appendChild(link)
link
}
linkP10.removeAttribute("href")
linkP10.addEventListener("click", EventListener {
entry.pending = true
launch {
val s = entry.schedule.get()
ScheduleRepository.update(s.copy(time = s.time + 10))
}
})
linkDel = linkDel ?: run {
val link = createHtmlView<HTMLAnchorElement>()
link.classList.add("calendar-tools-del")
link.textContent = "del"
html.appendChild(link)
link
}
linkDel.removeAttribute("href")
linkDel.addEventListener("click", EventListener {
entry.pending = true
launch {
ScheduleRepository.delete(entry.scheduleId)
}
})
}
}
class CalendarEntry(view: HTMLElement) : View(view) {
private lateinit var mouseDelta: Point
private var newCell: CalendarCell? = null
private var language by dataset.property("language")
val scheduleId = dataset["id"]?.toLongOrNull() ?: 0
val schedule = RepositoryDelegate(ScheduleRepository, scheduleId)
var pending by classList.property("pending")
private fun onMove(event: MouseEvent) {
val position = event.toPoint() - mouseDelta
val cell = calendarCells.find {
position in it.dimension
}
if (cell != null) {
cell += this
if (newCell == null) {
style {
left = "0"
top = "0.1rem"
}
}
newCell = cell
}
event.preventDefault()
event.stopPropagation()
}
private fun onFinishMove(event: MouseEvent) {
classList -= "drag"
newCell?.let { cell ->
launch {
val newTime = cell.time
val newRoom = cell.getRoom()
pending = true
val s = schedule.get().copy(room = newRoom, time = newTime)
ScheduleRepository.update(s)
}
}
newCell = null
for (it in listeners) {
it.detach()
}
listeners = emptyList()
event.preventDefault()
event.stopPropagation()
}
private var listeners: List<de.westermann.kobserve.event.EventListener<*>> = emptyList()
init {
onMouseDown { event ->
if (event.target != html || "pending" in classList) {
event.stopPropagation()
return@onMouseDown
}
launch {
classList += "drag"
val s = schedule.get()
val time = s.time / CALENDAR_GRID_WIDTH * CALENDAR_GRID_WIDTH
val p = calendarCells.find {
it.day == s.day && it.time == time && it.roomId == s.room.id
}?.dimension?.center ?: dimension.center
mouseDelta = event.toPoint() - p
listeners = listOf(
Body.onMouseMove.reference(this::onMove),
Body.onMouseUp.reference(this::onFinishMove),
Body.onMouseLeave.reference(this::onFinishMove)
)
}
event.preventDefault()
event.stopPropagation()
}
var calendarTools: CalendarTools? = null
for (item in html.children) {
if (item.classList.contains("calendar-tools")) {
calendarTools = CalendarTools(this, item)
break
}
}
if (calendarTools == null) {
calendarTools = CalendarTools(this, createHtmlView())
html.appendChild(calendarTools.html)
}
}
fun load(schedule: Schedule) {
pending = false
language = schedule.workGroup.language.code
this.schedule.set(schedule)
style {
val size = schedule.workGroup.length / CALENDAR_GRID_WIDTH.toDouble()
val pos = (schedule.time % CALENDAR_GRID_WIDTH) / CALENDAR_GRID_WIDTH.toDouble()
val ps = "${pos * 100}%"
val sz = "${size * 100}%"
left = ps
top = "calc($ps + 0.1rem)"
width = sz
height = "calc($sz - 0.2rem)"
if (schedule.workGroup.track?.color != null) {
backgroundColor = schedule.workGroup.track.color.toString()
color = schedule.workGroup.track.color.calcTextColor().toString()
}
}
for (element in html.childNodes) {
if (element.isText) {
html.removeChild(element)
}
}
html.appendText(schedule.workGroup.name)
val time = schedule.time / CALENDAR_GRID_WIDTH * CALENDAR_GRID_WIDTH
val cell = calendarCells.find {
it.day == schedule.day && it.time == time && it.roomId == schedule.room.id
}
if (cell != null && cell.html != html.parentElement) {
cell += this
}
}
companion object {
fun create(schedule: Schedule): CalendarEntry {
val entry = CalendarEntry(createHtmlView())
entry.load(schedule)
return entry
}
}
}
class CalendarCell(view: HTMLElement) : ViewCollection<CalendarEntry>(view) {
val day = dataset["day"]?.toIntOrNull() ?: 0
val time = dataset["time"]?.toIntOrNull() ?: 0
val roomId = dataset["room"]?.toLongOrNull() ?: 0
private lateinit var room: Room
suspend fun getRoom(): Room {
if (this::room.isInitialized) {
return room
}
room = RoomRepository.get(roomId) ?: throw NoSuchElementException()
return room
}
}
var calendarEntries: List<CalendarEntry> = emptyList()
var calendarCells: List<CalendarCell> = emptyList()
fun initCalendar() {
calendarEntries = document.getElementsByClassName("calendar-entry")
.iterator().asSequence().map(::CalendarEntry).toList()
calendarCells = document.getElementsByClassName("calendar-cell")
.iterator().asSequence().filter { it.dataset["time"] != null }.map(::CalendarCell).toList()
ScheduleRepository.onCreate {
launch {
val schedule = ScheduleRepository.get(it) ?: throw NoSuchElementException()
CalendarEntry.create(schedule)
}
}
ScheduleRepository.onUpdate {
launch {
val schedule = ScheduleRepository.get(it) ?: throw NoSuchElementException()
var found = false
for (entry in calendarEntries) {
if (entry.scheduleId == it) {
entry.load(schedule)
found = true
}
}
if (!found) {
CalendarEntry.create(schedule)
}
}
}
ScheduleRepository.onDelete {
for (entry in calendarEntries) {
if (entry.scheduleId == it) {
entry.html.remove()
}
}
}
}

View file

@ -0,0 +1,32 @@
package de.kif.frontend.views
import de.kif.common.Search
import de.kif.common.SearchElement
import de.kif.frontend.iterator
import de.westermann.kwebview.components.InputView
import org.w3c.dom.HTMLFormElement
import org.w3c.dom.HTMLInputElement
import org.w3c.dom.HTMLTableElement
import org.w3c.dom.get
import kotlin.browser.document
fun initTableLayout() {
val form = document.getElementsByClassName("table-layout-search")[0] as HTMLFormElement
form.onsubmit = { false }
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")!!)
}
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"
}
}
}

View file

@ -0,0 +1,65 @@
package de.kif.frontend.views.calendar
import de.kif.frontend.iterator
import de.kif.frontend.launch
import de.kif.frontend.repository.ScheduleRepository
import de.westermann.kwebview.View
import org.w3c.dom.HTMLElement
import org.w3c.dom.get
import kotlin.browser.document
class Calendar(calendar: HTMLElement): View(calendar) {
var calendarEntries: List<CalendarEntry> = emptyList()
var calendarCells: List<CalendarCell> = emptyList()
val day: Int
init {
val editable = calendar.dataset["editable"]?.toBoolean() ?: false
day = calendar.dataset["day"]?.toIntOrNull() ?: -1
calendarEntries = document.getElementsByClassName("calendar-entry")
.iterator().asSequence().map{ CalendarEntry(this, it) }.onEach { it.editable = editable }.toList()
calendarCells = document.getElementsByClassName("calendar-cell")
.iterator().asSequence().filter { it.dataset["time"] != null }.map(::CalendarCell).toList()
if (editable) {
CalendarEdit(this, calendar.querySelector(".calendar-edit") as HTMLElement)
}
ScheduleRepository.onCreate {
launch {
val schedule = ScheduleRepository.get(it) ?: throw NoSuchElementException()
calendarEntries += CalendarEntry.create(this, schedule).also { it.editable = editable }
}
}
ScheduleRepository.onUpdate {
launch {
val schedule = ScheduleRepository.get(it) ?: throw NoSuchElementException()
var found = false
for (entry in calendarEntries) {
if (entry.scheduleId == it) {
entry.load(schedule)
found = true
}
}
if (!found) {
calendarEntries += CalendarEntry.create(this, schedule).also { it.editable = editable }
}
}
}
ScheduleRepository.onDelete {
for (entry in calendarEntries) {
if (entry.scheduleId == it) {
entry.html.remove()
}
}
}
}
}
fun initCalendar() {
Calendar(document.getElementsByClassName("calendar")[0] as? HTMLElement ?: return)
}

View file

@ -0,0 +1,28 @@
package de.kif.frontend.views.calendar
import de.kif.common.model.Room
import de.kif.frontend.repository.RoomRepository
import de.westermann.kwebview.ViewCollection
import org.w3c.dom.HTMLElement
import org.w3c.dom.get
class CalendarCell(view: HTMLElement) : ViewCollection<CalendarEntry>(view) {
val day = dataset["day"]?.toIntOrNull() ?: 0
val time = dataset["time"]?.toIntOrNull() ?: 0
val roomId = dataset["room"]?.toLongOrNull() ?: 0
private lateinit var room: Room
suspend fun getRoom(): Room {
if (this::room.isInitialized) {
return room
}
room = RoomRepository.get(roomId) ?: throw NoSuchElementException()
return room
}
init {
(view.getElementsByClassName("calendar-link")[0] as? HTMLElement)?.remove()
}
}

View file

@ -0,0 +1,76 @@
package de.kif.frontend.views.calendar
import de.kif.common.Search
import de.kif.frontend.launch
import de.kif.frontend.repository.WorkGroupRepository
import de.westermann.kobserve.list.filterObservable
import de.westermann.kobserve.list.observableListOf
import de.westermann.kobserve.list.sortObservable
import de.westermann.kwebview.View
import de.westermann.kwebview.components.Button
import de.westermann.kwebview.components.InputView
import de.westermann.kwebview.components.ListView
import de.westermann.kwebview.extra.listFactory
import org.w3c.dom.HTMLButtonElement
import org.w3c.dom.HTMLElement
import org.w3c.dom.HTMLInputElement
class CalendarEdit(
private val calendar: Calendar, view: HTMLElement
) : View(view) {
private val toggleEditButton =
Button.wrap(view.querySelector(".calendar-edit-top button") as HTMLButtonElement)
val search =
InputView.wrap(view.querySelector(".calendar-edit-search input") as HTMLInputElement)
val listView = ListView.wrap<CalendarWorkGroup>(
view.querySelector(".calendar-edit-list") as HTMLElement
)
private var loaded = false
val workGroupList = observableListOf<CalendarWorkGroup>()
private val sortedList = workGroupList.sortObservable(compareBy {
it.workGroup.name
}).filterObservable(search.valueProperty) { entry, search ->
val s = entry.workGroup.createSearch()
Search.match(search, s)
}
private fun load() {
if (loaded) return
loaded = true
launch {
for (workGroup in WorkGroupRepository.all()) {
workGroupList += CalendarWorkGroup(calendar, this, workGroup)
}
}
}
init {
toggleEditButton.onClick {
calendar.classList.toggle("edit")
if (!loaded) {
load()
}
}
WorkGroupRepository.onCreate {
if (loaded) {
launch {
workGroupList += CalendarWorkGroup(
calendar,
this,
WorkGroupRepository.get(it)!!
)
}
}
}
listView.listFactory(sortedList)
}
}

View file

@ -0,0 +1,222 @@
package de.kif.frontend.views.calendar
import de.kif.common.CALENDAR_GRID_WIDTH
import de.kif.common.model.Schedule
import de.kif.common.model.WorkGroup
import de.kif.frontend.iterator
import de.kif.frontend.launch
import de.kif.frontend.repository.RepositoryDelegate
import de.kif.frontend.repository.ScheduleRepository
import de.westermann.kwebview.Point
import de.westermann.kwebview.View
import de.westermann.kwebview.components.Body
import de.westermann.kwebview.createHtmlView
import de.westermann.kwebview.toPoint
import org.w3c.dom.HTMLElement
import org.w3c.dom.events.MouseEvent
import kotlin.dom.appendText
import kotlin.dom.isText
class CalendarEntry(private val calendar: Calendar, view: HTMLElement) : View(view) {
private lateinit var mouseDelta: Point
private var newCell: CalendarCell? = null
private var language by dataset.property("language")
var scheduleId = dataset["id"]?.toLongOrNull() ?: -1
val schedule =
RepositoryDelegate(ScheduleRepository, scheduleId)
private lateinit var workGroup: WorkGroup
var pending by classList.property("pending")
var editable: Boolean = false
private fun onMove(event: MouseEvent) {
val position = event.toPoint() - mouseDelta
val cell = calendar.calendarCells.find {
position in it.dimension
}
if (cell != null) {
cell += this
if (newCell == null) {
style {
left = "0"
top = "0.1rem"
}
}
newCell = cell
}
event.preventDefault()
event.stopPropagation()
}
private fun onFinishMove(event: MouseEvent) {
classList -= "drag"
newCell?.let { cell ->
launch {
val newTime = cell.time
val newRoom = cell.getRoom()
pending = true
if (scheduleId < 0) {
ScheduleRepository.create(
Schedule(
null,
workGroup,
newRoom,
calendar.day,
newTime
)
)
html.remove()
} else {
ScheduleRepository.update(schedule.get().copy(room = newRoom, time = newTime))
}
}
}
newCell = null
for (it in listeners) {
it.detach()
}
listeners = emptyList()
event.preventDefault()
event.stopPropagation()
}
private var listeners: List<de.westermann.kobserve.event.EventListener<*>> = emptyList()
fun startDrag() {
listeners = listOf(
Body.onMouseMove.reference(this::onMove),
Body.onMouseUp.reference(this::onFinishMove),
Body.onMouseLeave.reference(this::onFinishMove)
)
}
init {
onMouseDown { event ->
if (!editable || event.target != html || "pending" in classList) {
event.stopPropagation()
return@onMouseDown
}
launch {
classList += "drag"
val s = schedule.get()
val time = s.time / CALENDAR_GRID_WIDTH * CALENDAR_GRID_WIDTH
val p = calendar.calendarCells.find {
it.day == s.day && it.time == time && it.roomId == s.room.id
}?.dimension?.center ?: dimension.center
mouseDelta = event.toPoint() - p
startDrag()
}
event.preventDefault()
event.stopPropagation()
}
var calendarTools: CalendarTools? = null
for (item in html.children) {
if (item.classList.contains("calendar-tools")) {
calendarTools = CalendarTools(this, item)
break
}
}
if (calendarTools == null) {
calendarTools = CalendarTools(this, createHtmlView())
html.appendChild(calendarTools.html)
}
}
fun load(schedule: Schedule) {
pending = false
if (schedule.id != null) scheduleId = schedule.id
this.schedule.set(schedule)
style {
val pos = (schedule.time % CALENDAR_GRID_WIDTH) / CALENDAR_GRID_WIDTH.toDouble()
val ps = "${pos * 100}%"
left = ps
top = "calc($ps + 0.1rem)"
}
load(schedule.workGroup)
val time = schedule.time / CALENDAR_GRID_WIDTH * CALENDAR_GRID_WIDTH
val cell = calendar.calendarCells.find {
it.day == schedule.day && it.time == time && it.roomId == schedule.room.id
}
if (cell != null && cell.html != html.parentElement) {
cell += this
}
}
fun load(workGroup: WorkGroup) {
pending = false
language = workGroup.language.code
this.workGroup = workGroup
style {
val size = workGroup.length / CALENDAR_GRID_WIDTH.toDouble()
val sz = "${size * 100}%"
width = sz
height = "calc($sz - 0.2rem)"
if (workGroup.track?.color != null) {
backgroundColor = workGroup.track.color.toString()
color = workGroup.track.color.calcTextColor().toString()
}
}
for (element in html.childNodes) {
if (element.isText) {
html.removeChild(element)
}
}
html.appendText(workGroup.name)
}
companion object {
fun create(calendar: Calendar, schedule: Schedule): CalendarEntry {
val entry = CalendarEntry(calendar, createHtmlView())
entry.load(schedule)
return entry
}
fun create(calendar: Calendar, workGroup: WorkGroup): CalendarEntry {
val entry = CalendarEntry(calendar, createHtmlView())
entry.load(workGroup)
entry.mouseDelta = Point.ZERO
entry.startDrag()
return entry
}
}
}

View file

@ -0,0 +1,129 @@
package de.kif.frontend.views.calendar
import de.kif.common.CALENDAR_GRID_WIDTH
import de.kif.frontend.iterator
import de.kif.frontend.launch
import de.kif.frontend.repository.ScheduleRepository
import de.westermann.kwebview.View
import de.westermann.kwebview.createHtmlView
import org.w3c.dom.HTMLAnchorElement
import org.w3c.dom.HTMLElement
import org.w3c.dom.events.EventListener
class CalendarTools(entry: CalendarEntry, view: HTMLElement) : View(view) {
init {
var linkM10: HTMLAnchorElement? = null
var linkM5: HTMLAnchorElement? = null
var linkReset: HTMLAnchorElement? = null
var linkP5: HTMLAnchorElement? = null
var linkP10: HTMLAnchorElement? = null
var linkDel: HTMLAnchorElement? = null
for (element in html.children) {
when {
element.classList.contains("calendar-tools-m10") -> linkM10 = element as? HTMLAnchorElement
element.classList.contains("calendar-tools-m5") -> linkM5 = element as? HTMLAnchorElement
element.classList.contains("calendar-tools-reset") -> linkReset = element as? HTMLAnchorElement
element.classList.contains("calendar-tools-p5") -> linkP5 = element as? HTMLAnchorElement
element.classList.contains("calendar-tools-p10") -> linkP10 = element as? HTMLAnchorElement
element.classList.contains("calendar-tools-del") -> linkDel = element as? HTMLAnchorElement
}
}
linkM10 = linkM10 ?: run {
val link = createHtmlView<HTMLAnchorElement>()
link.classList.add("calendar-tools-m10")
link.textContent = "-10"
html.appendChild(link)
link
}
linkM10.removeAttribute("href")
linkM10.addEventListener("click", EventListener {
entry.pending = true
launch {
val s = entry.schedule.get()
ScheduleRepository.update(s.copy(time = s.time - 10))
}
})
linkM5 = linkM5 ?: run {
val link = createHtmlView<HTMLAnchorElement>()
link.classList.add("calendar-tools-m5")
link.textContent = "-5"
html.appendChild(link)
link
}
linkM5.removeAttribute("href")
linkM5.addEventListener("click", EventListener {
entry.pending = true
launch {
val s = entry.schedule.get()
ScheduleRepository.update(s.copy(time = s.time - 5))
}
})
linkReset = linkReset ?: run {
val link = createHtmlView<HTMLAnchorElement>()
link.classList.add("calendar-tools-reset")
link.textContent = "reset"
html.appendChild(link)
link
}
linkReset.removeAttribute("href")
linkReset.addEventListener("click", EventListener {
entry.pending = true
launch {
val s = entry.schedule.get()
ScheduleRepository.update(s.copy(time = s.time / CALENDAR_GRID_WIDTH * CALENDAR_GRID_WIDTH))
}
})
linkP5 = linkP5 ?: run {
val link = createHtmlView<HTMLAnchorElement>()
link.classList.add("calendar-tools-p5")
link.textContent = "+5"
html.appendChild(link)
link
}
linkP5.removeAttribute("href")
linkP5.addEventListener("click", EventListener {
entry.pending = true
launch {
val s = entry.schedule.get()
ScheduleRepository.update(s.copy(time = s.time + 5))
}
})
linkP10 = linkP10 ?: run {
val link = createHtmlView<HTMLAnchorElement>()
link.classList.add("calendar-tools-p10")
link.textContent = "+10"
html.appendChild(link)
link
}
linkP10.removeAttribute("href")
linkP10.addEventListener("click", EventListener {
entry.pending = true
launch {
val s = entry.schedule.get()
ScheduleRepository.update(s.copy(time = s.time + 10))
}
})
linkDel = linkDel ?: run {
val link = createHtmlView<HTMLAnchorElement>()
link.classList.add("calendar-tools-del")
link.textContent = "del"
html.appendChild(link)
link
}
linkDel.removeAttribute("href")
linkDel.addEventListener("click", EventListener {
entry.pending = true
launch {
ScheduleRepository.delete(entry.scheduleId)
}
})
}
}

View file

@ -0,0 +1,56 @@
package de.kif.frontend.views.calendar
import de.kif.common.model.WorkGroup
import de.kif.frontend.launch
import de.kif.frontend.repository.WorkGroupRepository
import de.westermann.kwebview.View
class CalendarWorkGroup(
private val calendar: Calendar,
private val calendarEdit: CalendarEdit,
workGroup: WorkGroup
) : View() {
private var language by dataset.property("language")
lateinit var workGroup: WorkGroup
private set
private fun load(workGroup: WorkGroup) {
this.workGroup = workGroup
html.textContent = workGroup.name
language = workGroup.language.code
style {
if (workGroup.track?.color != null) {
backgroundColor = workGroup.track.color.toString()
color = workGroup.track.color.calcTextColor().toString()
}
}
}
init {
load(workGroup)
val references: MutableList<de.westermann.kobserve.event.EventListener<*>> = mutableListOf()
references += WorkGroupRepository.onUpdate.reference {
if (it == workGroup.id) {
launch {
load(WorkGroupRepository.get(it)!!)
}
}
}
references += WorkGroupRepository.onDelete.reference {
if (it == workGroup.id) {
calendarEdit.workGroupList -= this
}
}
onMouseDown {
CalendarEntry.create(calendar, workGroup)
}
}
}

View file

@ -6,7 +6,7 @@ import kotlin.dom.clear
/** /**
* @author lars * @author lars
*/ */
abstract class ViewCollection<V : View>(view: HTMLElement = createHtmlView()) : View(view), Iterable<V> { abstract class ViewCollection<V : View>(view: HTMLElement = createHtmlView()) : View(view), Collection<V> {
private val children: MutableList<V> = mutableListOf() private val children: MutableList<V> = mutableListOf()
@ -29,6 +29,36 @@ abstract class ViewCollection<V : View>(view: HTMLElement = createHtmlView()) :
} }
} }
fun replace(oldView: V, newView: V) {
if (children.contains(oldView)) {
children.add(children.indexOf(oldView), newView)
html.insertBefore(newView.html, oldView.html)
children -= oldView
html.removeChild(oldView.html)
}
}
fun add(view: V) = append(view)
fun add(index: Int, view: V) {
if (index >= size) {
append(view)
} else {
html.insertBefore(view.html, children[index].html)
children.add(index, view)
}
}
operator fun get(index: Int): V {
return children[index]
}
fun removeAt(index: Int) {
if (index in 0 until size) {
remove(children[index])
}
}
fun toForeground(view: V) { fun toForeground(view: V) {
if (view in children && children.indexOf(view) < children.size - 1) { if (view in children && children.indexOf(view) < children.size - 1) {
remove(view) remove(view)
@ -48,8 +78,7 @@ abstract class ViewCollection<V : View>(view: HTMLElement = createHtmlView()) :
operator fun minusAssign(view: V) = remove(view) operator fun minusAssign(view: V) = remove(view)
val isEmpty: Boolean override fun isEmpty(): Boolean = children.isEmpty()
get() = children.isEmpty()
fun clear() { fun clear() {
children.clear() children.clear()
@ -58,16 +87,14 @@ abstract class ViewCollection<V : View>(view: HTMLElement = createHtmlView()) :
override fun iterator(): Iterator<V> = children.iterator() override fun iterator(): Iterator<V> = children.iterator()
val size: Int override val size: Int
get() = children.size get() = children.size
operator fun contains(view: V) = children.contains(view) override fun contains(element: V) = children.contains(element)
override fun containsAll(elements: Collection<V>): Boolean = children.containsAll(elements)
operator fun V.unaryPlus() { operator fun V.unaryPlus() {
append(this) append(this)
} }
companion object {
fun <V : View> wrap(htmlElement: HTMLElement) = object : ViewCollection<V>(htmlElement) {}
}
} }

View file

@ -5,7 +5,9 @@ import org.w3c.dom.HTMLInputElement
import kotlin.math.abs import kotlin.math.abs
import kotlin.random.Random import kotlin.random.Random
abstract class ViewForLabel : View(createHtmlView<HTMLInputElement>()) { abstract class ViewForLabel(
view: HTMLInputElement = createHtmlView()
) : View(view) {
override val html = super.html as HTMLInputElement override val html = super.html as HTMLInputElement
private var label: Label? = null private var label: Label? = null

View file

@ -14,7 +14,7 @@ import org.w3c.dom.HTMLButtonElement
* *
* @author lars * @author lars
*/ */
class Button() : ViewCollection<View>(createHtmlView<HTMLButtonElement>()) { class Button(view: HTMLButtonElement = createHtmlView()) : ViewCollection<View>(view) {
constructor(text: String) : this() { constructor(text: String) : this() {
this.text = text this.text = text
@ -38,6 +38,10 @@ class Button() : ViewCollection<View>(createHtmlView<HTMLButtonElement>()) {
} }
val textProperty: Property<String> = property(this::text) val textProperty: Property<String> = property(this::text)
companion object {
fun wrap(view: HTMLButtonElement) = Button(view)
}
} }
@KWebViewDsl @KWebViewDsl

View file

@ -3,8 +3,8 @@ 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.ValidationProperty import de.westermann.kobserve.ValidationProperty
import de.westermann.kobserve.property.property
import de.westermann.kobserve.not import de.westermann.kobserve.not
import de.westermann.kobserve.property.property
import de.westermann.kwebview.* import de.westermann.kwebview.*
import org.w3c.dom.HTMLInputElement import org.w3c.dom.HTMLInputElement
import org.w3c.dom.events.Event import org.w3c.dom.events.Event
@ -13,8 +13,9 @@ import org.w3c.dom.events.KeyboardEvent
class InputView( class InputView(
type: InputType, type: InputType,
initValue: String = "" initValue: String = "",
) : ViewForLabel() { view: HTMLInputElement = createHtmlView()
) : ViewForLabel(view) {
fun bind(property: ReadOnlyProperty<String>) { fun bind(property: ReadOnlyProperty<String>) {
valueProperty.bind(property) valueProperty.bind(property)
@ -108,12 +109,17 @@ class InputView(
html.addEventListener("keyup", changeListener) html.addEventListener("keyup", changeListener)
html.addEventListener("keypress", changeListener) html.addEventListener("keypress", changeListener)
} }
companion object {
fun wrap(view: HTMLInputElement) = InputView(InputType.SEARCH, view.value, view)
}
} }
enum class InputType(val html: String) { enum class InputType(val html: String) {
TEXT("text"), TEXT("text"),
NUMBER("number"), NUMBER("number"),
PASSWORD("password"); PASSWORD("password"),
SEARCH("search");
companion object { companion object {
fun find(html: String): InputType? = values().find { it.html == html } fun find(html: String): InputType? = values().find { it.html == html }
@ -138,17 +144,33 @@ fun ViewCollection<in InputView>.inputView(text: ValidationProperty<String>, ini
@KWebViewDsl @KWebViewDsl
fun ViewCollection<in InputView>.inputView(type: InputType = InputType.TEXT, text: String = "", init: InputView.() -> Unit = {}) = fun ViewCollection<in InputView>.inputView(
type: InputType = InputType.TEXT,
text: String = "",
init: InputView.() -> Unit = {}
) =
InputView(type, text).also(this::append).also(init) InputView(type, text).also(this::append).also(init)
@KWebViewDsl @KWebViewDsl
fun ViewCollection<in InputView>.inputView(type: InputType = InputType.TEXT, text: ReadOnlyProperty<String>, init: InputView.() -> Unit = {}) = fun ViewCollection<in InputView>.inputView(
type: InputType = InputType.TEXT,
text: ReadOnlyProperty<String>,
init: InputView.() -> Unit = {}
) =
InputView(type, text.value).also(this::append).also { it.bind(text) }.also(init) InputView(type, text.value).also(this::append).also { it.bind(text) }.also(init)
@KWebViewDsl @KWebViewDsl
fun ViewCollection<in InputView>.inputView(type: InputType = InputType.TEXT, text: Property<String>, init: InputView.() -> Unit = {}) = fun ViewCollection<in InputView>.inputView(
type: InputType = InputType.TEXT,
text: Property<String>,
init: InputView.() -> Unit = {}
) =
InputView(type, text.value).also(this::append).also { it.bind(text) }.also(init) InputView(type, text.value).also(this::append).also { it.bind(text) }.also(init)
@KWebViewDsl @KWebViewDsl
fun ViewCollection<in InputView>.inputView(type: InputType = InputType.TEXT, text: ValidationProperty<String>, init: InputView.() -> Unit = {}) = fun ViewCollection<in InputView>.inputView(
type: InputType = InputType.TEXT,
text: ValidationProperty<String>,
init: InputView.() -> Unit = {}
) =
InputView(type, text.value).also(this::append).also { it.bind(text) }.also(init) InputView(type, text.value).also(this::append).also { it.bind(text) }.also(init)

View file

@ -0,0 +1,30 @@
package de.westermann.kwebview.components
import de.westermann.kwebview.KWebViewDsl
import de.westermann.kwebview.View
import de.westermann.kwebview.ViewCollection
import de.westermann.kwebview.createHtmlView
import org.w3c.dom.HTMLDivElement
import org.w3c.dom.HTMLElement
class ListView<T : View>(view: HTMLElement = createHtmlView()) : ViewCollection<T>(view) {
override val html = super.html as HTMLDivElement
companion object {
fun <T : View> wrap(view: HTMLElement) = ListView<T>(view)
}
}
@KWebViewDsl
fun <T : View> ViewCollection<in ListView<T>>.listView(
vararg classes: String,
init: ListView<T>.() -> Unit = {}
): ListView<T> {
val view = ListView<T>()
for (c in classes) {
view.classList += c
}
append(view)
init(view)
return view
}

View file

@ -0,0 +1,62 @@
package de.westermann.kwebview.extra
import de.westermann.kobserve.list.ObservableReadOnlyList
import de.westermann.kwebview.View
import de.westermann.kwebview.ViewCollection
import de.westermann.kwebview.async
fun <T, V : View> ViewCollection<in V>.listFactory(
list: ObservableReadOnlyList<T>,
factory: (T) -> V,
animateAdd: Int? = null,
animateRemove: Int? = null
) {
for (element in list) {
+factory(element)
}
list.onAdd { (index, element) ->
val view = factory(element)
add(index, view)
if (animateAdd != null) {
classList += "animate-add"
view.classList += "active"
async(animateAdd) {
classList -= "animate-add"
view.classList -= "active"
}
}
}
list.onRemove { (index) ->
@Suppress("UNCHECKED_CAST") val view = this[index] as V
if (animateRemove == null) {
remove(view)
} else {
classList += "animate-remove"
view.classList += "active"
async(animateRemove) {
classList -= "animate-remove"
view.classList -= "active"
remove(view)
}
}
}
list.onUpdate { (oldIndex, newIndex, element) ->
removeAt(oldIndex)
add(newIndex, factory(element))
}
}
fun <V : View> ViewCollection<in V>.listFactory(
list: ObservableReadOnlyList<V>,
animateAdd: Int? = null,
animateRemove: Int? = null
) = listFactory(
list,
{ it },
animateAdd,
animateRemove
)

View file

@ -220,28 +220,6 @@ a {
.table-layout-search { .table-layout-search {
float: left; float: left;
padding-bottom: 0.5rem !important; padding-bottom: 0.5rem !important;
input {
padding-right: 4rem;
}
.btn-search {
position: absolute;
top: 0;
height: 2.5rem;
line-height: 2.5rem;
right: -3px;
border-bottom-left-radius: 0;
border-top-left-radius: 0;
margin-top: 1px;
margin-right: 2px;
}
input:focus ~ .btn-search {
border-color: $primary-color;
border-width: 2px;
}
} }
.table-layout-action { .table-layout-action {
@ -470,13 +448,106 @@ form {
width: 100%; width: 100%;
position: relative; position: relative;
margin-top: 1rem; margin-top: 1rem;
}
& > div { .calendar-table {
width: calc(100% - 6rem); float: left;
width: 100%;
overflow-x: scroll; overflow-x: scroll;
overflow-y: visible; overflow-y: visible;
margin-left: 6rem;
padding-bottom: 1rem; padding-bottom: 1rem;
position: relative;
transition: width $transitionTime;
}
.calendar-edit {
width: 16rem;
display: block;
position: absolute;
right: 0;
top: -3rem;
padding-top: 3rem;
overflow: hidden;
.calendar-edit-main {
position: relative;
left: 16rem;
opacity: 0;
transition: left $transitionTime, opacity$transitionTime;
}
}
.calendar-edit-top {
position: absolute;
right: 0;
top: 0;
}
.calendar-edit-search {
padding: 0.5rem;
}
.calendar-work-group {
position: relative;
display: block;
border-radius: 0.2rem;
line-height: 2rem;
font-size: 0.8rem;
white-space: nowrap;
text-overflow: ellipsis;
background-color: $primary-color;
color: $primary-text-color;
padding: 0 0.5rem;
margin: 0.5rem;
&::after {
content: attr(data-language);
bottom: 0;
right: 1rem;
opacity: 0.6;
position: absolute;
text-transform: uppercase;
}
&:hover .calendar-tools {
display: block;
}
&.drag {
box-shadow: 0 0.1rem 0.2rem rgba($text-primary-color, 0.8);
z-index: 2;
.calendar-tools {
display: none;
}
}
&.pending::before {
content: '';
display: block;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: $background-primary-color;
opacity: 0.6;
}
@include no-select()
}
.calendar[data-editable = "true"].edit {
.calendar-table {
width: calc(100% - 16rem);
border-right: solid 1px rgba($text-primary-color, 0.1);
}
.calendar-edit-main {
left: 0;
opacity: 1;
} }
} }
@ -507,6 +578,10 @@ form {
} }
} }
.calendar[data-editable=false] .calendar-tools {
display: none !important;
}
.calendar-entry { .calendar-entry {
position: absolute; position: absolute;
display: block; display: block;
@ -555,6 +630,8 @@ form {
background-color: $background-primary-color; background-color: $background-primary-color;
opacity: 0.6; opacity: 0.6;
} }
@include no-select()
} }
.calendar-table-time-to-room { .calendar-table-time-to-room {
@ -618,7 +695,6 @@ form {
} }
.calendar-cell:first-child { .calendar-cell:first-child {
position: absolute;
width: 6rem; width: 6rem;
left: 0; left: 0;
text-align: center; text-align: center;
@ -663,13 +739,6 @@ form {
line-height: 2rem; line-height: 2rem;
height: 1.3rem; height: 1.3rem;
&:nth-child(4n + 2)::before {
content: '';
position: absolute;
width: calc(100% - 6rem);
border-top: solid 1px rgba($text-primary-color, 0.1);
}
.calendar-cell { .calendar-cell {
position: relative; position: relative;
width: 12rem; width: 12rem;
@ -677,6 +746,7 @@ form {
&::before { &::before {
content: ''; content: '';
height: 100%; height: 100%;
width: 100%;
left: 0; left: 0;
top: 0; top: 0;
border-left: solid 1px rgba($text-primary-color, 0.1); border-left: solid 1px rgba($text-primary-color, 0.1);
@ -694,18 +764,22 @@ form {
left: 0.1rem !important; left: 0.1rem !important;
right: 0.1rem; right: 0.1rem;
} }
a {
position: relative;
}
} }
.calendar-cell:first-child:not(:empty)::before { .calendar-cell:first-child::before {
width: 100%;
top: 0;
border-left: none; border-left: none;
}
&:nth-child(4n + 2) .calendar-cell::before {
border-top: solid 1px rgba($text-primary-color, 0.1); border-top: solid 1px rgba($text-primary-color, 0.1);
} }
} }
.calendar-cell:first-child { .calendar-cell:first-child {
position: absolute;
width: 6rem; width: 6rem;
left: 0; left: 0;
text-align: center; text-align: center;

View file

@ -7,9 +7,7 @@ import de.kif.common.model.User
import io.ktor.application.Application import io.ktor.application.Application
import io.ktor.server.engine.embeddedServer import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty import io.ktor.server.netty.Netty
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlin.coroutines.suspendCoroutine
object Main { object Main {
@Suppress("UnusedMainParameter") @Suppress("UnusedMainParameter")
@ -34,6 +32,8 @@ object Main {
password = System.console()?.readPassword()?.toString() ?: readLine() password = System.console()?.readPassword()?.toString() ?: readLine()
} }
println("Create root user '$username' with pw '${"*".repeat(password.length)}'")
UserRepository.create( UserRepository.create(
User( User(
null, null,

View file

@ -5,11 +5,11 @@ import de.kif.backend.isAuthenticated
import de.kif.backend.repository.RoomRepository import de.kif.backend.repository.RoomRepository
import de.kif.backend.repository.ScheduleRepository import de.kif.backend.repository.ScheduleRepository
import de.kif.backend.repository.WorkGroupRepository import de.kif.backend.repository.WorkGroupRepository
import de.kif.backend.util.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.CALENDAR_GRID_WIDTH import de.kif.common.CALENDAR_GRID_WIDTH
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 de.kif.common.model.Schedule import de.kif.common.model.Schedule
@ -304,19 +304,21 @@ fun Route.calendar() {
a("/calendar/${day + 1}") { +">" } a("/calendar/${day + 1}") { +">" }
} }
div("header-right") { div("header-right") {
a("/calendar/$day/room-to-time") { a("/calendar/$day/rtt") {
+"Room to time" +"Room to time"
} }
a("/calendar/$day/time-to-room") { a("/calendar/$day/ttr") {
+"Time to room" +"Time to room"
} }
} }
} }
div("calendar") { div("calendar") {
val editable = user != null
attributes["data-day"] = day.toString() attributes["data-day"] = day.toString()
attributes["data-editable"] = editable.toString()
div { div("calendar-table") {
when (orientation) { when (orientation) {
CalendarOrientation.ROOM_TO_TIME -> renderRoomToTime( CalendarOrientation.ROOM_TO_TIME -> renderRoomToTime(
day, day,
@ -324,7 +326,7 @@ fun Route.calendar() {
max, max,
rooms, rooms,
schedules, schedules,
user != null editable
) )
CalendarOrientation.TIME_TO_ROOM -> renderTimeToRoom( CalendarOrientation.TIME_TO_ROOM -> renderTimeToRoom(
day, day,
@ -332,13 +334,33 @@ fun Route.calendar() {
max, max,
rooms, rooms,
schedules, schedules,
user != null editable
) )
} }
} }
}
}
if (editable) {
div("calendar-edit") {
div("calendar-edit-top") {
button(classes = "form-btn") {
+"Edit"
}
}
div("calendar-edit-main") {
div("calendar-edit-search") {
input(InputType.search, name = "search", classes = "form-control") {
placeholder = "Search"
value = ""
}
}
div("calendar-edit-list") {
}
}
}
}
}
}
} }
} }
@ -392,10 +414,11 @@ fun Route.calendar() {
} }
for (u in list) { for (u in list) {
if (Search.match(search, u.name)) { val s = u.createSearch()
if (Search.match(search, s)) {
val href = "/calendar/$day/${room.id}/$time/${u.id}" val href = "/calendar/$day/${room.id}/$time/${u.id}"
entry { entry {
attributes["data-search"] = Search.pack(u.name) attributes["data-search"] = s.stringify()
td { td {
a(href) { a(href) {
+u.name +u.name

View file

@ -2,7 +2,7 @@ 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.backend.util.Search 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
@ -64,9 +64,10 @@ fun Route.room() {
} }
for (u in list) { for (u in list) {
if (Search.match(search, u.name)) { val s = u.createSearch()
if (Search.match(search, s)) {
entry { entry {
attributes["data-search"] = Search.pack(u.name) attributes["data-search"] = s.stringify()
td { td {
+u.name +u.name
} }

View file

@ -3,7 +3,7 @@ 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.backend.util.Search 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
@ -118,9 +118,10 @@ fun Route.track() {
} }
for (u in list) { for (u in list) {
if (Search.match(search, u.name)) { val s = u.createSearch()
if (Search.match(search, s)) {
entry { entry {
attributes["data-search"] = Search.pack(u.name) attributes["data-search"] = s.stringify()
td { td {
+u.name +u.name
} }

View file

@ -4,7 +4,7 @@ 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.backend.util.Search 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
@ -66,9 +66,10 @@ fun Route.user() {
} }
for (u in list) { for (u in list) {
if (Search.match(search, u.username)) { val s = u.createSearch()
if (Search.match(search, s)) {
entry { entry {
attributes["data-search"] = Search.pack(u.username) attributes["data-search"] = s.stringify()
td { td {
+u.username +u.username
} }

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.repository.TrackRepository import de.kif.backend.repository.TrackRepository
import de.kif.backend.repository.WorkGroupRepository import de.kif.backend.repository.WorkGroupRepository
import de.kif.backend.util.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.Language import de.kif.common.model.Language
import de.kif.common.model.Permission import de.kif.common.model.Permission
import de.kif.common.model.WorkGroup import de.kif.common.model.WorkGroup
@ -20,12 +20,9 @@ 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.component2
import kotlin.collections.find
import kotlin.collections.firstOrNull
import kotlin.collections.mapValues
import kotlin.collections.set import kotlin.collections.set
fun Route.workGroup() { fun Route.workGroup() {
@ -84,9 +81,12 @@ fun Route.workGroup() {
} }
for (u in list) { for (u in list) {
if (Search.match(search, u.name)) { val s = u.createSearch()
entry { entry {
attributes["data-search"] = Search.pack(u.name) attributes["style"] = CSSBuilder().apply {
display = if (Search.match(search, s)) Display.tableRow else Display.none
}.toString()
attributes["data-search"] = s.stringify()
td { td {
+u.name +u.name
} }
@ -106,7 +106,7 @@ fun Route.workGroup() {
+u.length.toString() +u.length.toString()
} }
td { td {
+u.language.toString() +u.language.localeName
} }
td(classes = "action") { td(classes = "action") {
a("/workgroup/${u.id}") { a("/workgroup/${u.id}") {
@ -120,7 +120,6 @@ fun Route.workGroup() {
} }
} }
} }
}
get("/workgroup/{id}") { get("/workgroup/{id}") {
authenticateOrRedirect(Permission.WORK_GROUP) { user -> authenticateOrRedirect(Permission.WORK_GROUP) { user ->
@ -171,7 +170,7 @@ fun Route.workGroup() {
htmlFor = "track" htmlFor = "track"
+"Track" +"Track"
} }
div("input-group") { //div("input-group") {
select( select(
classes = "form-control" classes = "form-control"
) { ) {
@ -190,11 +189,12 @@ fun Route.workGroup() {
} }
} }
} }
/*
a("/tracks", classes = "form-btn") { a("/tracks", classes = "form-btn") {
i("material-icons") { +"edit" } i("material-icons") { +"edit" }
} }
} }
*/
} }
div("form-group") { div("form-group") {
label { label {

View file

@ -1,29 +0,0 @@
package de.kif.backend.util
object Search {
private fun permute(list: List<String>): List<List<String>> {
if (list.size <= 1) return listOf(list)
val perm = mutableListOf<List<String>>()
for (elem in list) {
val p = permute(list - elem)
for (x in p) {
perm += x + elem
}
}
return perm
}
fun match(search: String, vararg params: String): Boolean {
val s = search.toLowerCase().replace(" ", "")
return permute(params.map { it.toLowerCase().replace(" ", "") }).any {
it.joinToString("").contains(s)
}
}
fun pack(vararg params: String): String = params.joinToString("|") { it.toLowerCase() }
}

View file

@ -15,6 +15,7 @@ class TableTemplate() : Template<FlowContent> {
override fun FlowContent.apply() { override fun FlowContent.apply() {
div("table-layout") { div("table-layout") {
form(classes = "form-group table-layout-search") { form(classes = "form-group table-layout-search") {
div("input-group") {
input(InputType.search, name = "search", classes = "form-control") { input(InputType.search, name = "search", classes = "form-control") {
placeholder = "Search" placeholder = "Search"
value = searchValue value = searchValue
@ -23,6 +24,7 @@ class TableTemplate() : Template<FlowContent> {
i("material-icons") { +"search" } i("material-icons") { +"search" }
} }
} }
}
div("table-layout-action") { div("table-layout-action") {
insert(action) insert(action)
} }

View file

@ -0,0 +1,75 @@
POST http://localhost:8080/api/authenticate
Content-Type: application/json
{
"username": "admin",
"password": "admin"
}
> {%
client.assert(response.body.OK === true, "Login failed!");
client.global.set("auth_token", response.headers.valueOf("Set-Cookie"));
%}
###
POST http://localhost:8080/api/rooms
Content-Type: application/json
Cookie: {{auth_token}}
{
"name": "E006",
"places": 20,
"projector": true
}
###
POST http://localhost:8080/api/rooms
Content-Type: application/json
Cookie: {{auth_token}}
{
"name": "E007",
"places": 20,
"projector": true
}
###
POST http://localhost:8080/api/rooms
Content-Type: application/json
Cookie: {{auth_token}}
{
"name": "E008",
"places": 20,
"projector": true
}
###
POST http://localhost:8080/api/rooms
Content-Type: application/json
Cookie: {{auth_token}}
{
"name": "E009",
"places": 20,
"projector": true
}
###
POST http://localhost:8080/api/rooms
Content-Type: application/json
Cookie: {{auth_token}}
{
"name": "E010",
"places": 20,
"projector": true
}
###