Add post auto update

This commit is contained in:
Lars Westermann 2019-05-25 17:37:17 +02:00
parent 2be6c7c819
commit 74c297263e
Signed by: lars.westermann
GPG key ID: 9D417FA5BB9D5E1D
13 changed files with 354 additions and 40 deletions

View file

@ -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,

View file

@ -61,7 +61,8 @@ class WebSocketClient() {
ScheduleRepository.handler,
TrackRepository.handler,
UserRepository.handler,
WorkGroupRepository.handler
WorkGroupRepository.handler,
PostRepository.handler
)
init {

View file

@ -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()
}
}

View file

@ -82,6 +82,30 @@ suspend fun repositoryPost(
}
}
suspend fun repositoryRawGet(
url: String
): String {
console.log("GET: $url")
val promise = Promise<String> { 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 <T> Promise<T>.await(): T = suspendCoroutine { cont ->
then({ cont.resume(it) }, { cont.resumeWithException(it) })
}

View file

@ -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<Post> {
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): 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<Post> {
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)
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}
}

View file

@ -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

View file

@ -11,7 +11,11 @@ import org.w3c.dom.HTMLAnchorElement
*
* @author lars
*/
class Link(target: String) : ViewCollection<View>(createHtmlView<HTMLAnchorElement>("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<View>(createHtmlView<HTMLAnchorEleme
html.href = value
}
init {
this.target = target
companion object {
fun wrap(view: HTMLAnchorElement) = Link(view)
}
}

View file

@ -1036,6 +1036,7 @@ form {
.overview-post {
max-height: 20rem;
overflow: hidden;
margin-bottom: 1rem;
&::after {
content: '';

View file

@ -56,6 +56,7 @@ fun Application.main() {
userApi()
workGroupApi()
constraintsApi()
postApi()
// Web socket push notifications
pushService()

View file

@ -20,6 +20,29 @@ import io.ktor.routing.post
import io.ktor.util.toMap
import kotlinx.html.*
fun DIV.createPost(post: Post, editable: Boolean = false, additionalClasses: String = "") {
var classes = "post"
if (additionalClasses.isNotBlank()) {
classes += " $additionalClasses"
}
div(classes) {
attributes["data-id"] = post.id.toString()
a("/p/${post.url}", classes = "post-name") {
+post.name
}
if (editable) {
a("/post/${post.id}", classes = "post-edit") {
i("material-icons") { +"edit" }
}
}
div("post-content") {
unsafe {
raw(markdownToHtml(post.content))
}
}
}
}
fun Route.overview() {
get("") {
val user = isAuthenticated(Permission.POST)
@ -44,21 +67,7 @@ fun Route.overview() {
}
for (post in postList) {
div("overview-post post") {
a("/p/${post.url}", classes="post-name") {
+post.name
}
if (editable) {
a("/post/${post.id}", classes = "post-edit") {
i("material-icons") { +"edit" }
}
}
div("post-content") {
unsafe {
raw(markdownToHtml(post.content))
}
}
}
createPost(post, editable, "overview-post")
}
}
div("overview-side") {
@ -109,22 +118,8 @@ fun Route.overview() {
}
content {
div("overview") {
div("post") {
span("post-name") {
+post.name
}
if (editable) {
a("/post/${post.id}", classes = "post-edit") {
i("material-icons") { +"edit" }
}
}
div("post-content") {
unsafe {
raw(markdownToHtml(post.content))
}
}
}
}
createPost(post, editable)
}
}
}
}

View file

@ -0,0 +1,115 @@
package de.kif.backend.route.api
import de.kif.backend.authenticate
import de.kif.backend.repository.PostRepository
import de.kif.backend.util.markdownToHtml
import de.kif.common.model.Permission
import de.kif.common.model.Post
import io.ktor.application.call
import io.ktor.html.respondHtml
import io.ktor.http.HttpStatusCode
import io.ktor.request.receive
import io.ktor.response.respond
import io.ktor.routing.Route
import io.ktor.routing.get
import io.ktor.routing.post
import kotlinx.html.unsafe
fun Route.postApi() {
get("/api/posts") {
try {
val posts = PostRepository.all()
call.success(posts)
} catch (_: Exception) {
call.error(HttpStatusCode.InternalServerError)
}
}
post("/api/posts") {
try {
authenticate(Permission.POST) {
val post = call.receive<Post>()
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<Post>().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)
}
}
}