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 serialization_version = '0.11.0'
def observable_version = '0.9.3'
kotlin {
jvm() {
@ -53,7 +54,7 @@ kotlin {
commonMain {
dependencies {
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"
}
@ -84,7 +85,7 @@ kotlin {
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 'ch.qos.logback:logback-classic:1.2.3'
@ -103,7 +104,7 @@ kotlin {
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 {

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
interface Model
import de.kif.common.SearchElement
interface Model {
fun createSearch(): SearchElement
}

View file

@ -1,5 +1,6 @@
package de.kif.common.model
import de.kif.common.SearchElement
import kotlinx.serialization.Serializable
@Serializable
@ -8,4 +9,15 @@ data class Room(
val name: String,
val places: Int,
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
import de.kif.common.SearchElement
import kotlinx.serialization.Serializable
@Serializable
@ -9,4 +10,16 @@ data class Schedule(
val room: Room,
val day: 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
import de.kif.common.SearchElement
import kotlinx.serialization.Serializable
@Serializable
data class Track(
val id: Long?,
var name: String,
var color: Color
) : Model
val name: String,
val color: Color
) : Model {
override fun createSearch() = SearchElement(
mapOf(
"name" to name
)
)
}

View file

@ -1,5 +1,6 @@
package de.kif.common.model
import de.kif.common.SearchElement
import kotlinx.serialization.Serializable
@Serializable
@ -13,4 +14,10 @@ data class User(
fun checkPermission(permission: Permission): Boolean {
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
import de.kif.common.SearchElement
import kotlinx.serialization.Serializable
@Serializable
@ -12,4 +13,19 @@ data class WorkGroup(
val resolution: Boolean,
val length: Int,
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
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 kotlin.browser.document
@ -10,4 +11,7 @@ fun main() = init {
if (document.getElementsByClassName("calendar").length > 0) {
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 {
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!")
}

View file

@ -23,7 +23,7 @@ object ScheduleRepository : Repository<Schedule> {
}
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!")
}

View file

@ -23,7 +23,7 @@ object TrackRepository : Repository<Track> {
}
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!")
}

View file

@ -23,7 +23,7 @@ object UserRepository : Repository<User> {
}
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!")
}

View file

@ -23,7 +23,7 @@ object WorkGroupRepository : Repository<WorkGroup> {
}
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!")
}

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
*/
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()
@ -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) {
if (view in children && children.indexOf(view) < children.size - 1) {
remove(view)
@ -48,8 +78,7 @@ abstract class ViewCollection<V : View>(view: HTMLElement = createHtmlView()) :
operator fun minusAssign(view: V) = remove(view)
val isEmpty: Boolean
get() = children.isEmpty()
override fun isEmpty(): Boolean = children.isEmpty()
fun clear() {
children.clear()
@ -58,16 +87,14 @@ abstract class ViewCollection<V : View>(view: HTMLElement = createHtmlView()) :
override fun iterator(): Iterator<V> = children.iterator()
val size: Int
override val size: Int
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() {
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.random.Random
abstract class ViewForLabel : View(createHtmlView<HTMLInputElement>()) {
abstract class ViewForLabel(
view: HTMLInputElement = createHtmlView()
) : View(view) {
override val html = super.html as HTMLInputElement
private var label: Label? = null

View file

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

View file

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

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 {
float: left;
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 {
@ -470,13 +448,106 @@ form {
width: 100%;
position: relative;
margin-top: 1rem;
}
& > div {
width: calc(100% - 6rem);
overflow-x: scroll;
overflow-y: visible;
margin-left: 6rem;
padding-bottom: 1rem;
.calendar-table {
float: left;
width: 100%;
overflow-x: scroll;
overflow-y: visible;
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 {
position: absolute;
display: block;
@ -555,6 +630,8 @@ form {
background-color: $background-primary-color;
opacity: 0.6;
}
@include no-select()
}
.calendar-table-time-to-room {
@ -618,7 +695,6 @@ form {
}
.calendar-cell:first-child {
position: absolute;
width: 6rem;
left: 0;
text-align: center;
@ -663,13 +739,6 @@ form {
line-height: 2rem;
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 {
position: relative;
width: 12rem;
@ -677,6 +746,7 @@ form {
&::before {
content: '';
height: 100%;
width: 100%;
left: 0;
top: 0;
border-left: solid 1px rgba($text-primary-color, 0.1);
@ -694,18 +764,22 @@ form {
left: 0.1rem !important;
right: 0.1rem;
}
a {
position: relative;
}
}
.calendar-cell:first-child:not(:empty)::before {
width: 100%;
top: 0;
.calendar-cell:first-child::before {
border-left: none;
}
&:nth-child(4n + 2) .calendar-cell::before {
border-top: solid 1px rgba($text-primary-color, 0.1);
}
}
.calendar-cell:first-child {
position: absolute;
width: 6rem;
left: 0;
text-align: center;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,10 +4,10 @@ package de.kif.backend.route
import de.kif.backend.authenticateOrRedirect
import de.kif.backend.repository.TrackRepository
import de.kif.backend.repository.WorkGroupRepository
import de.kif.backend.util.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.Language
import de.kif.common.model.Permission
import de.kif.common.model.WorkGroup
@ -20,12 +20,9 @@ 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.find
import kotlin.collections.firstOrNull
import kotlin.collections.mapValues
import kotlin.collections.set
fun Route.workGroup() {
@ -84,34 +81,36 @@ fun Route.workGroup() {
}
for (u in list) {
if (Search.match(search, u.name)) {
entry {
attributes["data-search"] = Search.pack(u.name)
td {
+u.name
}
td {
+u.interested.toString()
}
td {
+(u.track?.name ?: "")
}
td {
+u.projector.toString()
}
td {
+u.resolution.toString()
}
td {
+u.length.toString()
}
td {
+u.language.toString()
}
td(classes = "action") {
a("/workgroup/${u.id}") {
i("material-icons") { +"edit" }
}
val s = u.createSearch()
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.interested.toString()
}
td {
+(u.track?.name ?: "")
}
td {
+u.projector.toString()
}
td {
+u.resolution.toString()
}
td {
+u.length.toString()
}
td {
+u.language.localeName
}
td(classes = "action") {
a("/workgroup/${u.id}") {
i("material-icons") { +"edit" }
}
}
}
@ -171,30 +170,31 @@ fun Route.workGroup() {
htmlFor = "track"
+"Track"
}
div("input-group") {
select(
classes = "form-control"
) {
name = "track"
//div("input-group") {
select(
classes = "form-control"
) {
name = "track"
option {
selected = (editWorkGroup.track?.id ?: -1) < 0
value = "-1"
+"None"
}
for (track in tracks) {
option {
selected = (editWorkGroup.track?.id ?: -1) < 0
value = "-1"
+"None"
}
for (track in tracks) {
option {
selected = editWorkGroup.track?.id == track.id
value = track.id.toString()
+track.name
}
selected = editWorkGroup.track?.id == track.id
value = track.id.toString()
+track.name
}
}
}
/*
a("/tracks", classes = "form-btn") {
i("material-icons") { +"edit" }
}
}
*/
}
div("form-group") {
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,12 +15,14 @@ class TableTemplate() : Template<FlowContent> {
override fun FlowContent.apply() {
div("table-layout") {
form(classes = "form-group table-layout-search") {
input(InputType.search, name = "search", classes = "form-control") {
placeholder = "Search"
value = searchValue
}
button(type = ButtonType.submit, classes = "form-btn btn-search") {
i("material-icons") { +"search" }
div("input-group") {
input(InputType.search, name = "search", classes = "form-control") {
placeholder = "Search"
value = searchValue
}
button(type = ButtonType.submit, classes = "form-btn btn-search") {
i("material-icons") { +"search" }
}
}
}
div("table-layout-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
}
###