Add post auto update
This commit is contained in:
parent
2be6c7c819
commit
74c297263e
13 changed files with 354 additions and 40 deletions
|
@ -1,8 +1,10 @@
|
||||||
package de.kif.common.model
|
package de.kif.common.model
|
||||||
|
|
||||||
import de.kif.common.SearchElement
|
import de.kif.common.SearchElement
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
@Serializable
|
||||||
data class Post(
|
data class Post(
|
||||||
override val id: Long? = null,
|
override val id: Long? = null,
|
||||||
val name: String,
|
val name: String,
|
||||||
|
|
|
@ -61,7 +61,8 @@ class WebSocketClient() {
|
||||||
ScheduleRepository.handler,
|
ScheduleRepository.handler,
|
||||||
TrackRepository.handler,
|
TrackRepository.handler,
|
||||||
UserRepository.handler,
|
UserRepository.handler,
|
||||||
WorkGroupRepository.handler
|
WorkGroupRepository.handler,
|
||||||
|
PostRepository.handler
|
||||||
)
|
)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
package de.kif.frontend
|
package de.kif.frontend
|
||||||
|
|
||||||
import de.kif.frontend.views.calendar.initCalendar
|
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.initWorkGroupConstraints
|
||||||
|
import de.kif.frontend.views.overview.initOverviewMain
|
||||||
|
import de.kif.frontend.views.overview.initPosts
|
||||||
import de.westermann.kwebview.components.init
|
import de.westermann.kwebview.components.init
|
||||||
import kotlin.browser.document
|
import kotlin.browser.document
|
||||||
|
|
||||||
|
@ -18,4 +20,10 @@ fun main() = init {
|
||||||
if (document.getElementsByClassName("work-group-constraints").length > 0) {
|
if (document.getElementsByClassName("work-group-constraints").length > 0) {
|
||||||
initWorkGroupConstraints()
|
initWorkGroupConstraints()
|
||||||
}
|
}
|
||||||
|
if (document.getElementsByClassName("overview-main").length > 0) {
|
||||||
|
initOverviewMain()
|
||||||
|
}
|
||||||
|
if (document.getElementsByClassName("post").length > 0) {
|
||||||
|
initPosts()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 ->
|
suspend fun <T> Promise<T>.await(): T = suspendCoroutine { cont ->
|
||||||
then({ cont.resume(it) }, { cont.resumeWithException(it) })
|
then({ cont.resume(it) }, { cont.resumeWithException(it) })
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
69
src/jsMain/kotlin/de/kif/frontend/views/overview/PostView.kt
Normal file
69
src/jsMain/kotlin/de/kif/frontend/views/overview/PostView.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,6 @@
|
||||||
package de.kif.frontend.views
|
package de.kif.frontend.views.table
|
||||||
|
|
||||||
import de.kif.frontend.iterator
|
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 de.westermann.kwebview.components.InputView
|
||||||
import org.w3c.dom.HTMLFormElement
|
import org.w3c.dom.HTMLFormElement
|
||||||
import org.w3c.dom.HTMLInputElement
|
import org.w3c.dom.HTMLInputElement
|
|
@ -11,7 +11,11 @@ import org.w3c.dom.HTMLAnchorElement
|
||||||
*
|
*
|
||||||
* @author lars
|
* @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
|
override val html = super.html as HTMLAnchorElement
|
||||||
|
|
||||||
|
@ -27,8 +31,8 @@ class Link(target: String) : ViewCollection<View>(createHtmlView<HTMLAnchorEleme
|
||||||
html.href = value
|
html.href = value
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
companion object {
|
||||||
this.target = target
|
fun wrap(view: HTMLAnchorElement) = Link(view)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1036,6 +1036,7 @@ form {
|
||||||
.overview-post {
|
.overview-post {
|
||||||
max-height: 20rem;
|
max-height: 20rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
content: '';
|
content: '';
|
||||||
|
|
|
@ -56,6 +56,7 @@ fun Application.main() {
|
||||||
userApi()
|
userApi()
|
||||||
workGroupApi()
|
workGroupApi()
|
||||||
constraintsApi()
|
constraintsApi()
|
||||||
|
postApi()
|
||||||
|
|
||||||
// Web socket push notifications
|
// Web socket push notifications
|
||||||
pushService()
|
pushService()
|
||||||
|
|
|
@ -20,6 +20,29 @@ import io.ktor.routing.post
|
||||||
import io.ktor.util.toMap
|
import io.ktor.util.toMap
|
||||||
import kotlinx.html.*
|
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() {
|
fun Route.overview() {
|
||||||
get("") {
|
get("") {
|
||||||
val user = isAuthenticated(Permission.POST)
|
val user = isAuthenticated(Permission.POST)
|
||||||
|
@ -44,21 +67,7 @@ fun Route.overview() {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (post in postList) {
|
for (post in postList) {
|
||||||
div("overview-post post") {
|
createPost(post, editable, "overview-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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
div("overview-side") {
|
div("overview-side") {
|
||||||
|
@ -109,21 +118,7 @@ fun Route.overview() {
|
||||||
}
|
}
|
||||||
content {
|
content {
|
||||||
div("overview") {
|
div("overview") {
|
||||||
div("post") {
|
createPost(post, editable)
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
115
src/jvmMain/kotlin/de/kif/backend/route/api/Post.kt
Normal file
115
src/jvmMain/kotlin/de/kif/backend/route/api/Post.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue