From 74c297263e96eac0b580ef7db2c09a286ccc88a4 Mon Sep 17 00:00:00 2001 From: Lars Westermann Date: Sat, 25 May 2019 17:37:17 +0200 Subject: [PATCH] Add post auto update --- .../kotlin/de/kif/common/model/Post.kt | 2 + .../kotlin/de/kif/frontend/WebSocketClient.kt | 3 +- src/jsMain/kotlin/de/kif/frontend/main.kt | 10 +- .../kotlin/de/kif/frontend/repository/Ajax.kt | 24 ++++ .../kif/frontend/repository/PostRepository.kt | 56 +++++++++ .../frontend/views/overview/OverviewMain.kt | 41 +++++++ .../kif/frontend/views/overview/PostView.kt | 69 +++++++++++ .../frontend/views/{ => table}/TableLayout.kt | 5 +- .../de/westermann/kwebview/components/Link.kt | 10 +- src/jsMain/resources/style/style.scss | 1 + .../kotlin/de/kif/backend/Application.kt | 1 + .../kotlin/de/kif/backend/route/Overview.kt | 57 ++++----- .../kotlin/de/kif/backend/route/api/Post.kt | 115 ++++++++++++++++++ 13 files changed, 354 insertions(+), 40 deletions(-) create mode 100644 src/jsMain/kotlin/de/kif/frontend/repository/PostRepository.kt create mode 100644 src/jsMain/kotlin/de/kif/frontend/views/overview/OverviewMain.kt create mode 100644 src/jsMain/kotlin/de/kif/frontend/views/overview/PostView.kt rename src/jsMain/kotlin/de/kif/frontend/views/{ => table}/TableLayout.kt (85%) create mode 100644 src/jvmMain/kotlin/de/kif/backend/route/api/Post.kt diff --git a/src/commonMain/kotlin/de/kif/common/model/Post.kt b/src/commonMain/kotlin/de/kif/common/model/Post.kt index 1bf4904..d739f61 100644 --- a/src/commonMain/kotlin/de/kif/common/model/Post.kt +++ b/src/commonMain/kotlin/de/kif/common/model/Post.kt @@ -1,8 +1,10 @@ package de.kif.common.model import de.kif.common.SearchElement +import kotlinx.serialization.Serializable import kotlin.random.Random +@Serializable data class Post( override val id: Long? = null, val name: String, diff --git a/src/jsMain/kotlin/de/kif/frontend/WebSocketClient.kt b/src/jsMain/kotlin/de/kif/frontend/WebSocketClient.kt index 5839f85..b7470e9 100644 --- a/src/jsMain/kotlin/de/kif/frontend/WebSocketClient.kt +++ b/src/jsMain/kotlin/de/kif/frontend/WebSocketClient.kt @@ -61,7 +61,8 @@ class WebSocketClient() { ScheduleRepository.handler, TrackRepository.handler, UserRepository.handler, - WorkGroupRepository.handler + WorkGroupRepository.handler, + PostRepository.handler ) init { diff --git a/src/jsMain/kotlin/de/kif/frontend/main.kt b/src/jsMain/kotlin/de/kif/frontend/main.kt index 83515c4..cd77097 100644 --- a/src/jsMain/kotlin/de/kif/frontend/main.kt +++ b/src/jsMain/kotlin/de/kif/frontend/main.kt @@ -1,8 +1,10 @@ package de.kif.frontend import de.kif.frontend.views.calendar.initCalendar -import de.kif.frontend.views.initTableLayout +import de.kif.frontend.views.table.initTableLayout import de.kif.frontend.views.initWorkGroupConstraints +import de.kif.frontend.views.overview.initOverviewMain +import de.kif.frontend.views.overview.initPosts import de.westermann.kwebview.components.init import kotlin.browser.document @@ -18,4 +20,10 @@ fun main() = init { if (document.getElementsByClassName("work-group-constraints").length > 0) { initWorkGroupConstraints() } + if (document.getElementsByClassName("overview-main").length > 0) { + initOverviewMain() + } + if (document.getElementsByClassName("post").length > 0) { + initPosts() + } } diff --git a/src/jsMain/kotlin/de/kif/frontend/repository/Ajax.kt b/src/jsMain/kotlin/de/kif/frontend/repository/Ajax.kt index f113d8c..d1f7d00 100644 --- a/src/jsMain/kotlin/de/kif/frontend/repository/Ajax.kt +++ b/src/jsMain/kotlin/de/kif/frontend/repository/Ajax.kt @@ -82,6 +82,30 @@ suspend fun repositoryPost( } } +suspend fun repositoryRawGet( + url: String +): String { + console.log("GET: $url") + val promise = Promise { resolve, reject -> + val xhttp = XMLHttpRequest() + + xhttp.onreadystatechange = { + if (xhttp.readyState == 4.toShort()) { + if (xhttp.status == 200.toShort() || xhttp.status == 304.toShort()) { + resolve(xhttp.responseText) + } else { + reject(NoSuchElementException("${xhttp.status}: ${xhttp.statusText}")) + } + } + } + xhttp.open("GET", url, true) + + xhttp.send() + } + + return promise.await() +} + suspend fun Promise.await(): T = suspendCoroutine { cont -> then({ cont.resume(it) }, { cont.resumeWithException(it) }) } diff --git a/src/jsMain/kotlin/de/kif/frontend/repository/PostRepository.kt b/src/jsMain/kotlin/de/kif/frontend/repository/PostRepository.kt new file mode 100644 index 0000000..0facc43 --- /dev/null +++ b/src/jsMain/kotlin/de/kif/frontend/repository/PostRepository.kt @@ -0,0 +1,56 @@ +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.Post +import de.kif.frontend.MessageHandler +import de.westermann.kobserve.event.EventHandler +import kotlinx.serialization.DynamicObjectParser +import kotlinx.serialization.list + +object PostRepository : Repository { + + override val onCreate = EventHandler() + override val onUpdate = EventHandler() + override val onDelete = EventHandler() + + private val parser = DynamicObjectParser() + + override suspend fun get(id: Long): Post? { + val json = repositoryGet("/api/post/$id") ?: return null + return parser.parse(json, Post.serializer()) + } + + override suspend fun create(model: Post): Long { + return repositoryPost("/api/posts", Message.json.stringify(Post.serializer(), model)) + ?: throw IllegalStateException("Cannot create model!") + } + + override suspend fun update(model: Post) { + if (model.id == null) throw IllegalStateException("Cannot update model which was not created!") + repositoryPost("/api/post/${model.id}", Message.json.stringify(Post.serializer(), model)) + } + + override suspend fun delete(id: Long) { + repositoryPost("/api/post/$id/delete") + } + + override suspend fun all(): List { + val json = repositoryGet("/api/posts") ?: return emptyList() + return parser.parse(json, Post.serializer().list) + } + + suspend fun htmlByUrl(url: String): String { + return repositoryRawGet("/api/p/$url") + } + + val handler = object : MessageHandler(RepositoryType.POST) { + + override fun onCreate(id: Long) = onCreate.emit(id) + + override fun onUpdate(id: Long) = onUpdate.emit(id) + + override fun onDelete(id: Long) = onDelete.emit(id) + } +} diff --git a/src/jsMain/kotlin/de/kif/frontend/views/overview/OverviewMain.kt b/src/jsMain/kotlin/de/kif/frontend/views/overview/OverviewMain.kt new file mode 100644 index 0000000..be6ad5e --- /dev/null +++ b/src/jsMain/kotlin/de/kif/frontend/views/overview/OverviewMain.kt @@ -0,0 +1,41 @@ +package de.kif.frontend.views.overview + +import de.kif.frontend.iterator +import de.kif.frontend.repository.PostRepository +import org.w3c.dom.HTMLElement +import org.w3c.dom.get +import kotlin.browser.document + +fun initOverviewMain() { + val main = document.getElementsByClassName("overview-main")[0] as HTMLElement + + PostRepository.onCreate { + val post = PostView.create(it) + post.classList += "overview-post" + + val first = main.firstElementChild as? HTMLElement + + if (first == null) { + main.appendChild(post.html) + return@onCreate + } + if (first.classList.contains("post")) { + main.insertBefore(post.html, first) + return@onCreate + } + + val next = first.nextElementSibling as? HTMLElement + if (next == null) { + main.appendChild(post.html) + } else { + main.insertBefore(post.html, next) + } + } +} + +fun initPosts() { + val postList = document.getElementsByClassName("post") + for (post in postList) { + PostView(post) + } +} \ No newline at end of file diff --git a/src/jsMain/kotlin/de/kif/frontend/views/overview/PostView.kt b/src/jsMain/kotlin/de/kif/frontend/views/overview/PostView.kt new file mode 100644 index 0000000..2861685 --- /dev/null +++ b/src/jsMain/kotlin/de/kif/frontend/views/overview/PostView.kt @@ -0,0 +1,69 @@ +package de.kif.frontend.views.overview + +import de.kif.common.model.Post +import de.kif.frontend.launch +import de.kif.frontend.repository.PostRepository +import de.westermann.kwebview.View +import de.westermann.kwebview.components.Link +import de.westermann.kwebview.createHtmlView +import org.w3c.dom.HTMLAnchorElement +import org.w3c.dom.HTMLElement +import org.w3c.dom.get +import org.w3c.dom.set +import kotlin.browser.document + +class PostView( + view: HTMLElement +) : View(view) { + + private var postId = dataset["id"]?.toLongOrNull() ?: -1 + + private val nameView: Link + private val contentView: View + + private fun reload() { + launch { + val p = PostRepository.get(postId) ?: return@launch + + nameView.text = p.name + nameView.target = "/p/${p.url}" + + contentView.html.innerHTML = PostRepository.htmlByUrl(p.url) + } + } + + init { + val nameHtml = view.getElementsByClassName("post-name")[0] + nameView = nameHtml?.let { Link.wrap(it as HTMLAnchorElement) } ?: Link().also { + html.appendChild(it.html) + it.classList += "post-name" + } + + // val editHtml = view.getElementsByClassName("post-edit")[0] + // editView = editHtml?.let { Link.wrap(it as HTMLAnchorElement) } ?: Link() + + val contentHtml = view.getElementsByClassName("post-content")[0] + contentView = contentHtml?.let { wrap(it as HTMLElement) } ?: wrap(createHtmlView()).also { + html.appendChild(it.html) + it.classList += "post-content" + } + + PostRepository.onUpdate { + if (it == postId) { + reload() + } + } + PostRepository.onDelete { + html.remove() + } + } + + companion object { + fun create(postId: Long): PostView { + val div = document.createElement("div") as HTMLElement + div.classList.add("post") + div.dataset["id"] = postId.toString() + return PostView(div).also(PostView::reload) + } + } +} diff --git a/src/jsMain/kotlin/de/kif/frontend/views/TableLayout.kt b/src/jsMain/kotlin/de/kif/frontend/views/table/TableLayout.kt similarity index 85% rename from src/jsMain/kotlin/de/kif/frontend/views/TableLayout.kt rename to src/jsMain/kotlin/de/kif/frontend/views/table/TableLayout.kt index 9904e6d..c55aed2 100644 --- a/src/jsMain/kotlin/de/kif/frontend/views/TableLayout.kt +++ b/src/jsMain/kotlin/de/kif/frontend/views/table/TableLayout.kt @@ -1,9 +1,6 @@ -package de.kif.frontend.views +package de.kif.frontend.views.table import de.kif.frontend.iterator -import de.kif.frontend.views.table.RoomTableLine -import de.kif.frontend.views.table.TableLine -import de.kif.frontend.views.table.WorkGroupTableLine import de.westermann.kwebview.components.InputView import org.w3c.dom.HTMLFormElement import org.w3c.dom.HTMLInputElement diff --git a/src/jsMain/kotlin/de/westermann/kwebview/components/Link.kt b/src/jsMain/kotlin/de/westermann/kwebview/components/Link.kt index 014a6c9..f6e00a3 100644 --- a/src/jsMain/kotlin/de/westermann/kwebview/components/Link.kt +++ b/src/jsMain/kotlin/de/westermann/kwebview/components/Link.kt @@ -11,7 +11,11 @@ import org.w3c.dom.HTMLAnchorElement * * @author lars */ -class Link(target: String) : ViewCollection(createHtmlView("a")) { +class Link(view: HTMLAnchorElement = createHtmlView()) : View(view) { + + constructor(target: String, view: HTMLAnchorElement = createHtmlView()): this(view) { + this.target = target + } override val html = super.html as HTMLAnchorElement @@ -27,8 +31,8 @@ class Link(target: String) : ViewCollection(createHtmlView() + + val id = PostRepository.create(post) + + call.success(mapOf("id" to id)) + } onFailure { + call.error(HttpStatusCode.Unauthorized) + } + } catch (_: Exception) { + call.error(HttpStatusCode.InternalServerError) + } + } + + get("/api/post/{id}") { + try { + val id = call.parameters["id"]?.toLongOrNull() + val post = id?.let { PostRepository.get(it) } + + if (post != null) { + call.success(post) + } else { + call.error(HttpStatusCode.NotFound) + } + } catch (_: Exception) { + call.error(HttpStatusCode.InternalServerError) + } + } + + post("/api/post/{id}") { + try { + authenticate { + val id = call.parameters["id"]?.toLongOrNull() + val post = call.receive().copy(id = id) + + if (post.id != null) { + PostRepository.update(post) + call.success() + } else { + call.error(HttpStatusCode.NotFound) + } + } onFailure { + call.error(HttpStatusCode.Unauthorized) + } + } catch (_: Exception) { + call.error(HttpStatusCode.InternalServerError) + } + } + post("/api/post/{id}/delete") { + try { + authenticate { + val id = call.parameters["id"]?.toLongOrNull() + + if (id != null) { + PostRepository.delete(id) + call.success() + } else { + call.error(HttpStatusCode.NotFound) + } + } onFailure { + call.error(HttpStatusCode.Unauthorized) + } + } catch (_: Exception) { + call.error(HttpStatusCode.InternalServerError) + } + } + + get("/api/p/{url}") { + try { + val id = call.parameters["url"] + val post = id?.let { PostRepository.getByUrl(it) } + + if (post != null) { + call.respondHtml(HttpStatusCode.OK) { + unsafe { + raw(markdownToHtml(post.content)) + } + } + } else { + call.error(HttpStatusCode.NotFound) + } + } catch (_: Exception) { + call.error(HttpStatusCode.InternalServerError) + } + } +}