Add repositories

- Make model classes imutable
- Move model classes to common module
- Add repositories for backend and frontend
  - Backend repositories communicates with sqlite
  - Frontend repositories commuincates with rest api
- Add rest api
  - Authentication via cookies
- Add coroutines to forntend
- Add change listener to repositories
- Transmit change events to frontend repositories via websocket
- Switch to server side sessions
- Move setup from ui to cli
This commit is contained in:
Lars Westermann 2019-05-12 16:40:21 +02:00
parent 4e5dc610a3
commit b7d6476a70
Signed by: lars.westermann
GPG key ID: 9D417FA5BB9D5E1D
66 changed files with 2854 additions and 2336 deletions

View file

@ -0,0 +1,76 @@
package de.kif.frontend
import de.kif.common.Message
import de.kif.common.MessageType
import de.kif.common.RepositoryType
import de.kif.frontend.repository.*
import de.westermann.kwebview.async
import org.w3c.dom.MessageEvent
import org.w3c.dom.WebSocket
import org.w3c.dom.events.Event
import kotlin.browser.window
class WebSocketClient() {
private val url = "ws://${window.location.host}/"
private lateinit var ws: WebSocket
@Suppress("UNUSED_PARAMETER")
private fun onOpen(event: Event) {
console.log("Connected!")
}
private fun onMessage(messageEvent: MessageEvent) {
val message = Message.parse(messageEvent.data?.toString() ?: "")
for (handler in messageHandlers) {
if (handler.repository == message.repository) {
when (message.type) {
MessageType.CREATE -> handler.onCreate(message.id)
MessageType.UPDATE -> handler.onUpdate(message.id)
MessageType.DELETE -> handler.onDelete(message.id)
}
}
}
}
@Suppress("UNUSED_PARAMETER")
private fun onClose(event: Event) {
console.log("Disconnected!")
async(1000) {
connect()
}
}
@Suppress("UNUSED_PARAMETER")
private fun onError(event: Event) {
console.log("An error occurred!")
}
private fun connect() {
ws = WebSocket(url)
ws.onopen = this::onOpen
ws.onmessage = this::onMessage
ws.onclose = this::onClose
ws.onerror = this::onError
}
private val messageHandlers: List<MessageHandler> = listOf(
RoomRepository.handler,
ScheduleRepository.handler,
TrackRepository.handler,
UserRepository.handler,
WorkGroupRepository.handler
)
init {
connect()
}
}
abstract class MessageHandler(val repository: RepositoryType) {
abstract fun onCreate(id: Long)
abstract fun onUpdate(id: Long)
abstract fun onDelete(id: Long)
}

View file

@ -1,11 +0,0 @@
package de.kif.frontend.calendar
import de.westermann.kwebview.View
import de.westermann.kwebview.ViewCollection
import de.westermann.kwebview.createHtmlView
class Calendar : ViewCollection<View>(createHtmlView()) {
init {
}
}

View file

@ -0,0 +1,39 @@
package de.kif.frontend
import org.w3c.dom.*
import kotlin.coroutines.Continuation
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.coroutines.startCoroutine
operator fun HTMLCollection.iterator() = object : Iterator<HTMLElement> {
private var index = 0
override fun hasNext(): Boolean {
return index < this@iterator.length
}
override fun next(): HTMLElement {
return this@iterator.get(index++) as HTMLElement
}
}
operator fun NodeList.iterator() = object : Iterator<Node> {
private var index = 0
override fun hasNext(): Boolean {
return index < this@iterator.length
}
override fun next(): Node {
return this@iterator.get(index++)!!
}
}
fun launch(context: CoroutineContext = EmptyCoroutineContext, block: suspend () -> Unit) =
block.startCoroutine(Continuation(context) { result ->
result.onFailure { exception ->
console.error(exception)
}
})

View file

@ -1,381 +1,13 @@
package de.kif.frontend
import de.westermann.kobserve.property.mapBinding
import de.westermann.kwebview.*
import de.westermann.kwebview.components.Body
import de.kif.frontend.views.initCalendar
import de.westermann.kwebview.components.init
import kif.common.model.*
import org.w3c.dom.*
import org.w3c.dom.events.EventListener
import org.w3c.dom.events.MouseEvent
import kotlin.browser.document
import kotlin.browser.window
import kotlin.collections.Iterator
import kotlin.collections.List
import kotlin.collections.emptyList
import kotlin.collections.filter
import kotlin.collections.find
import kotlin.collections.forEach
import kotlin.collections.listOf
import kotlin.collections.minus
import kotlin.collections.plus
import kotlin.dom.appendText
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 {
classList += "pending"
get("/calendar/${entry.day}/${entry.room}/${entry.time}/-1") {
get("/calendar/${entry.day}/${entry.room}/${entry.timeId - 10}/${entry.workGroup}") {
println("success")
}
}
})
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 {
classList += "pending"
get("/calendar/${entry.day}/${entry.room}/${entry.time}/-1") {
get("/calendar/${entry.day}/${entry.room}/${entry.timeId - 5}/${entry.workGroup}") {
println("success")
}
}
})
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 {
classList += "pending"
get("/calendar/${entry.day}/${entry.room}/${entry.time}/-1") {
get("/calendar/${entry.day}/${entry.room}/${entry.cellTime}/${entry.workGroup}") {
println("success")
}
}
})
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 {
classList += "pending"
get("/calendar/${entry.day}/${entry.room}/${entry.time}/-1") {
get("/calendar/${entry.day}/${entry.room}/${entry.timeId + 5}/${entry.workGroup}") {
println("success")
}
}
})
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 {
classList += "pending"
get("/calendar/${entry.day}/${entry.room}/${entry.time}/-1") {
get("/calendar/${entry.day}/${entry.room}/${entry.timeId + 10}/${entry.workGroup}") {
println("success")
}
}
})
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 {
classList += "pending"
get("/calendar/${entry.day}/${entry.room}/${entry.time}/-1") {
println("success")
}
})
}
}
class CalendarEntry(view: HTMLElement) : View(view) {
private lateinit var mouseDelta: Point
private var newCell: CalendarCell? = null
var day by dataset.property("day")
val dayId by dataset.property("day").mapBinding { it?.toIntOrNull() ?: 0 }
var time by dataset.property("time")
val timeId by dataset.property("time").mapBinding { it?.toIntOrNull() ?: 0 }
var room by dataset.property("room")
val roomId by dataset.property("room").mapBinding { it?.toIntOrNull() ?: 0 }
var cellTime by dataset.property("cellTime")
var language by dataset.property("language")
var workGroup by dataset.property("workgroup")
val workGroupId by dataset.property("workgroup").mapBinding { it?.toIntOrNull() ?: 0 }
private fun onMove(event: MouseEvent) {
val position = event.toPoint() - mouseDelta
newCell?.classList?.remove("drag")
val cell = calendarCells.find {
position in it.dimension
}
if (cell != null) {
cell.classList.add("drag")
cell += this
newCell = cell
}
event.preventDefault()
event.stopPropagation()
}
private fun onFinishMove(event: MouseEvent) {
classList -= "drag"
newCell?.let { cell ->
cell.classList -= "drag"
val newTime = cell.time
val newRoom = cell.room
val day =
(document.getElementsByClassName("calendar")[0] as? HTMLElement)?.dataset?.get("day")?.toIntOrNull()
?: 0
classList += "pending"
get("/calendar/$day/$room/$time/-1") {
get("/calendar/$day/$newRoom/$newTime/$workGroup") {
println("success")
}
}
}
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
}
classList += "drag"
mouseDelta = event.toPoint() - point
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)
}
}
companion object {
fun create(
day: Int,
time: Int,
cellTime: Int,
room: Int,
workGroupId: Int,
workGroupName: String,
workGroupLength: Int,
workGroupLanguage: String,
workGroupColor: Color?
): CalendarEntry {
val entry = CalendarEntry(createHtmlView())
entry.day = day.toString()
entry.time = time.toString()
entry.cellTime = cellTime.toString()
entry.room = room.toString()
entry.workGroup = workGroupId.toString()
entry.language = workGroupLanguage
if (workGroupColor != null) {
entry.style {
val size = workGroupLength / CALENDAR_GRID_WIDTH.toDouble()
val pos = (time % CALENDAR_GRID_WIDTH) / CALENDAR_GRID_WIDTH.toDouble()
val pct = "${pos * 100}%"
val sz = "${size * 100}%"
left = pct
top = "calc($pct + 0.1rem)"
width = sz
height = "calc($sz - 0.2rem)"
backgroundColor = workGroupColor.toString()
color = workGroupColor.textColor.toString()
}
}
entry.html.appendText(workGroupName)
return entry
}
}
}
class CalendarCell(view: HTMLElement) : ViewCollection<CalendarEntry>(view) {
var day by dataset.property("day")
val dayId by dataset.property("day").mapBinding { it?.toIntOrNull() ?: 0 }
var time by dataset.property("time")
val timeId by dataset.property("time").mapBinding { it?.toIntOrNull() ?: 0 }
var room by dataset.property("room")
val roomId by dataset.property("room").mapBinding { it?.toIntOrNull() ?: 0 }
}
var calendarEntries: List<CalendarEntry> = emptyList()
var calendarCells: List<CalendarCell> = emptyList()
fun main() = init {
WebSocketClient()
val ws = WebSocket("ws://${window.location.host}/".also { println(it) })
ws.onmessage = {
val messageWrapper = Message.parse(it.data?.toString() ?: "")
val message = messageWrapper.message
println(message)
when (message) {
is MessageCreateCalendarEntry -> {
val entry = CalendarEntry.create(
message.day,
message.time,
message.cellTime,
message.room,
message.workGroupId,
message.workGroupName,
message.workGroupLength,
message.workGroupLanguage,
message.workGroupColor
)
for (cell in calendarCells) {
if (cell.dayId == message.day && cell.timeId == message.cellTime && cell.roomId == message.room) {
cell.html.appendChild(entry.html)
calendarEntries += entry
break
}
}
}
is MessageDeleteCalendarEntry -> {
val remove = calendarEntries.filter { entry ->
entry.dayId == message.day &&
entry.timeId == message.time &&
entry.roomId == message.roomId &&
entry.workGroupId == message.workGroupId
}
calendarEntries -= remove
remove.forEach {
it.html.remove()
}
}
else -> {
}
}
if (document.getElementsByClassName("calendar").length > 0) {
initCalendar()
}
ws.onopen = {
console.log("yes!")
}
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()
}
operator fun HTMLCollection.iterator() = object : Iterator<HTMLElement> {
private var index = 0
override fun hasNext(): Boolean {
return index < this@iterator.length
}
override fun next(): HTMLElement {
return this@iterator.get(index++) as HTMLElement
}
}

View file

@ -0,0 +1,106 @@
package de.kif.frontend.repository
import de.kif.common.Repository
import de.kif.common.model.Model
import org.w3c.xhr.XMLHttpRequest
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
import kotlin.js.Promise
suspend fun repositoryGet(
url: String
): dynamic {
console.log("GET: $url")
val promise = Promise<dynamic> { resolve, reject ->
val xhttp = XMLHttpRequest()
xhttp.onreadystatechange = {
if (xhttp.readyState == 4.toShort()) {
if (xhttp.status == 200.toShort() || xhttp.status == 304.toShort()) {
resolve(JSON.parse(xhttp.responseText))
} else {
reject(NoSuchElementException("${xhttp.status}: ${xhttp.statusText}"))
}
}
}
xhttp.open("GET", url, true)
xhttp.send()
}
try {
val d = promise.await()
return if (d.OK) {
d.data
} else {
null
}
} catch (e: NoSuchElementException) {
console.error(e)
return null
}
}
suspend fun repositoryPost(
url: String,
data: String? = null
): dynamic {
console.log("POST: $url", data)
val promise = Promise<dynamic> { resolve, reject ->
val xhttp = XMLHttpRequest()
xhttp.onreadystatechange = {
if (xhttp.readyState == 4.toShort()) {
if (xhttp.status == 200.toShort() || xhttp.status == 304.toShort()) {
resolve(JSON.parse(xhttp.responseText))
} else {
reject(NoSuchElementException("${xhttp.status}: ${xhttp.statusText}"))
}
}
}
xhttp.open("POST", url, true)
xhttp.setRequestHeader("Content-type", "application/json");
xhttp.send(data)
}
try {
val d = promise.await()
return if (d.OK) {
d.data
} else {
null
}
} catch (e: NoSuchElementException) {
console.error(e)
return null
}
}
suspend fun <T> Promise<T>.await(): T = suspendCoroutine { cont ->
then({ cont.resume(it) }, { cont.resumeWithException(it) })
}
class RepositoryDelegate<T : Model>(
private val repository: Repository<T>,
private val id: Long
) {
private var backing: T? = null
suspend fun get(): T {
if (backing == null) {
backing = repository.get(id) ?: throw NoSuchElementException()
}
return backing!!
}
fun set(value: T) {
backing = value
}
}

View file

@ -0,0 +1,52 @@
package de.kif.frontend.repository
import de.kif.common.Message
import de.kif.common.Repository
import de.kif.common.RepositoryType
import de.kif.common.model.Room
import de.kif.frontend.MessageHandler
import de.westermann.kobserve.event.EventHandler
import kotlinx.serialization.DynamicObjectParser
import kotlinx.serialization.list
object RoomRepository : Repository<Room> {
override val onCreate = EventHandler<Long>()
override val onUpdate = EventHandler<Long>()
override val onDelete = EventHandler<Long>()
private val parser = DynamicObjectParser()
override suspend fun get(id: Long): Room? {
val json = repositoryGet("/api/room/$id") ?: return null
return parser.parse(json, Room.serializer())
}
override suspend fun create(model: Room): Long {
return repositoryPost("/api/rooms", Message.json.stringify(Room.serializer(), model))?.toLong()
?: throw IllegalStateException("Cannot create model!")
}
override suspend fun update(model: Room) {
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
repositoryPost("/api/room/${model.id}", Message.json.stringify(Room.serializer(), model))
}
override suspend fun delete(id: Long) {
repositoryPost("/api/room/$id/delete")
}
override suspend fun all(): List<Room> {
val json = repositoryGet("/api/rooms") ?: return emptyList()
return parser.parse(json, Room.serializer().list)
}
val handler = object : MessageHandler(RepositoryType.ROOM) {
override fun onCreate(id: Long) = onCreate.emit(id)
override fun onUpdate(id: Long) = onUpdate.emit(id)
override fun onDelete(id: Long) = onDelete.emit(id)
}
}

View file

@ -0,0 +1,52 @@
package de.kif.frontend.repository
import de.kif.common.Message
import de.kif.common.Repository
import de.kif.common.RepositoryType
import de.kif.common.model.Schedule
import de.kif.frontend.MessageHandler
import de.westermann.kobserve.event.EventHandler
import kotlinx.serialization.DynamicObjectParser
import kotlinx.serialization.list
object ScheduleRepository : Repository<Schedule> {
override val onCreate = EventHandler<Long>()
override val onUpdate = EventHandler<Long>()
override val onDelete = EventHandler<Long>()
private val parser = DynamicObjectParser()
override suspend fun get(id: Long): Schedule? {
val json = repositoryGet("/api/schedule/$id") ?: return null
return parser.parse(json, Schedule.serializer())
}
override suspend fun create(model: Schedule): Long {
return repositoryPost("/api/schedules", Message.json.stringify(Schedule.serializer(), model))?.toLong()
?: throw IllegalStateException("Cannot create model!")
}
override suspend fun update(model: Schedule) {
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
repositoryPost("/api/schedule/${model.id}", Message.json.stringify(Schedule.serializer(), model))
}
override suspend fun delete(id: Long) {
repositoryPost("/api/schedule/$id/delete")
}
override suspend fun all(): List<Schedule> {
val json = repositoryGet("/api/schedules") ?: return emptyList()
return parser.parse(json, Schedule.serializer().list)
}
val handler = object : MessageHandler(RepositoryType.SCHEDULE) {
override fun onCreate(id: Long) = onCreate.emit(id)
override fun onUpdate(id: Long) = onUpdate.emit(id)
override fun onDelete(id: Long) = onDelete.emit(id)
}
}

View file

@ -0,0 +1,52 @@
package de.kif.frontend.repository
import de.kif.common.Message
import de.kif.common.Repository
import de.kif.common.RepositoryType
import de.kif.common.model.Track
import de.kif.frontend.MessageHandler
import de.westermann.kobserve.event.EventHandler
import kotlinx.serialization.DynamicObjectParser
import kotlinx.serialization.list
object TrackRepository : Repository<Track> {
override val onCreate = EventHandler<Long>()
override val onUpdate = EventHandler<Long>()
override val onDelete = EventHandler<Long>()
private val parser = DynamicObjectParser()
override suspend fun get(id: Long): Track? {
val json = repositoryGet("/api/track/$id") ?: return null
return parser.parse(json, Track.serializer())
}
override suspend fun create(model: Track): Long {
return repositoryPost("/api/tracks", Message.json.stringify(Track.serializer(), model))?.toLong()
?: throw IllegalStateException("Cannot create model!")
}
override suspend fun update(model: Track) {
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
repositoryPost("/api/track/${model.id}", Message.json.stringify(Track.serializer(), model))
}
override suspend fun delete(id: Long) {
repositoryPost("/api/track/$id/delete")
}
override suspend fun all(): List<Track> {
val json = repositoryGet("/api/tracks") ?: return emptyList()
return parser.parse(json, Track.serializer().list)
}
val handler = object : MessageHandler(RepositoryType.TRACK) {
override fun onCreate(id: Long) = onCreate.emit(id)
override fun onUpdate(id: Long) = onUpdate.emit(id)
override fun onDelete(id: Long) = onDelete.emit(id)
}
}

View file

@ -0,0 +1,52 @@
package de.kif.frontend.repository
import de.kif.common.Message
import de.kif.common.Repository
import de.kif.common.RepositoryType
import de.kif.common.model.User
import de.kif.frontend.MessageHandler
import de.westermann.kobserve.event.EventHandler
import kotlinx.serialization.DynamicObjectParser
import kotlinx.serialization.list
object UserRepository : Repository<User> {
override val onCreate = EventHandler<Long>()
override val onUpdate = EventHandler<Long>()
override val onDelete = EventHandler<Long>()
private val parser = DynamicObjectParser()
override suspend fun get(id: Long): User? {
val json = repositoryGet("/api/user/$id") ?: return null
return parser.parse(json, User.serializer())
}
override suspend fun create(model: User): Long {
return repositoryPost("/api/users", Message.json.stringify(User.serializer(), model))?.toLong()
?: throw IllegalStateException("Cannot create model!")
}
override suspend fun update(model: User) {
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
repositoryPost("/api/user/${model.id}", Message.json.stringify(User.serializer(), model))
}
override suspend fun delete(id: Long) {
repositoryPost("/api/user/$id/delete")
}
override suspend fun all(): List<User> {
val json = repositoryGet("/api/users") ?: return emptyList()
return parser.parse(json, User.serializer().list)
}
val handler = object : MessageHandler(RepositoryType.USER) {
override fun onCreate(id: Long) = onCreate.emit(id)
override fun onUpdate(id: Long) = onUpdate.emit(id)
override fun onDelete(id: Long) = onDelete.emit(id)
}
}

View file

@ -0,0 +1,52 @@
package de.kif.frontend.repository
import de.kif.common.Message
import de.kif.common.Repository
import de.kif.common.RepositoryType
import de.kif.common.model.WorkGroup
import de.kif.frontend.MessageHandler
import de.westermann.kobserve.event.EventHandler
import kotlinx.serialization.DynamicObjectParser
import kotlinx.serialization.list
object WorkGroupRepository : Repository<WorkGroup> {
override val onCreate = EventHandler<Long>()
override val onUpdate = EventHandler<Long>()
override val onDelete = EventHandler<Long>()
private val parser = DynamicObjectParser()
override suspend fun get(id: Long): WorkGroup? {
val json = repositoryGet("/api/workgroup/$id") ?: return null
return parser.parse(json, WorkGroup.serializer())
}
override suspend fun create(model: WorkGroup): Long {
return repositoryPost("/api/workgroups", Message.json.stringify(WorkGroup.serializer(), model))?.toLong()
?: throw IllegalStateException("Cannot create model!")
}
override suspend fun update(model: WorkGroup) {
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
repositoryPost("/api/workgroup/${model.id}", Message.json.stringify(WorkGroup.serializer(), model))
}
override suspend fun delete(id: Long) {
repositoryPost("/api/workgroup/$id/delete")
}
override suspend fun all(): List<WorkGroup> {
val json = repositoryGet("/api/workgroups") ?: return emptyList()
return parser.parse(json, WorkGroup.serializer().list)
}
val handler = object : MessageHandler(RepositoryType.WORK_GROUP) {
override fun onCreate(id: Long) = onCreate.emit(id)
override fun onUpdate(id: Long) = onUpdate.emit(id)
override fun onDelete(id: Long) = onDelete.emit(id)
}
}

View file

@ -0,0 +1,359 @@
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

@ -3,6 +3,7 @@ package de.westermann.kwebview
import de.westermann.kobserve.event.EventListener
import de.westermann.kobserve.Property
import de.westermann.kobserve.ReadOnlyProperty
import de.westermann.kobserve.property.property
import org.w3c.dom.DOMTokenList
/**
@ -97,6 +98,23 @@ class ClassList(
)
}
fun property(clazz: String): Property<Boolean> {
if (clazz in bound) {
throw IllegalArgumentException("Class is already bound!")
}
val property = property(get(clazz))
bound[clazz] = Bound(property,
property.onChange.reference {
list.toggle(clazz, property.value)
}
)
return property
}
fun unbind(clazz: String) {
if (clazz !in bound) {
throw IllegalArgumentException("Class is not bound!")

View file

@ -7,10 +7,10 @@ import kotlin.math.min
* @author lars
*/
data class Dimension(
val left: Double,
val top: Double,
val width: Double = 0.0,
val height: Double = 0.0
val left: Double,
val top: Double,
val width: Double = 0.0,
val height: Double = 0.0
) {
constructor(position: Point, size: Point = Point.ZERO) : this(position.x, position.y, size.x, size.y)
@ -27,12 +27,15 @@ data class Dimension(
val bottom: Double
get() = top + height
val center: Point
get() = Point(left + width / 2.0, top + height / 2.0)
val edges: Set<Point>
get() = setOf(
Point(left, top),
Point(right, top),
Point(left, bottom),
Point(right, bottom)
Point(left, top),
Point(right, top),
Point(left, bottom),
Point(right, bottom)
)
val normalized: Dimension

View file

@ -110,7 +110,7 @@ fun get(
}
}
fun post(
fun postForm(
url: String,
data: Map<String, String> = emptyMap(),
onError: (Int) -> Unit = {},
@ -141,3 +141,43 @@ fun post(
xhttp.send()
}
}
fun postJson(
url: String,
data: dynamic,
onError: (Int) -> Unit = {},
onSuccess: (String) -> Unit = {}
) {
val xhttp = XMLHttpRequest()
xhttp.onreadystatechange = {
if (xhttp.readyState == 4.toShort()) {
console.log(xhttp.status)
if (xhttp.status == 200.toShort() || xhttp.status == 304.toShort()) {
onSuccess(xhttp.responseText)
} else {
onError(xhttp.status.toInt())
}
}
}
xhttp.open("POST", url, true)
if (data.isNotEmpty()) {
xhttp.setRequestHeader("Content-type", "application/json");
xhttp.send(data)
} else {
xhttp.send()
}
}
fun jsonObject(block: (dynamic) -> Unit): dynamic {
val json = js("{}")
block(json)
return json
}
fun jsonArray(block: (dynamic) -> Unit): dynamic {
val json = js("[]")
block(json)
return json
}