From 5d9ca85b222a9e1298a35f40c6d8af2f5f8149b2 Mon Sep 17 00:00:00 2001 From: Hubert Van De Walle Date: Mon, 17 Aug 2020 21:18:01 +0200 Subject: [PATCH] Trash feature done --- .../main/kotlin/controllers/NoteController.kt | 24 +++++- app/src/main/kotlin/routes/Router.kt | 2 + app/src/main/kotlin/views/NoteView.kt | 76 +++++++++++-------- app/src/main/kotlin/views/View.kt | 4 +- .../views/components/DeletedNoteTable.kt | 51 +++++++++++++ .../main/kotlin/views/components/Navbar.kt | 9 ++- .../main/kotlin/views/components/NoteTable.kt | 39 ++++++++++ css/src/navbar.pcss | 33 ++++++++ css/src/note-table.pcss | 17 +++++ css/src/other.pcss | 8 -- css/src/pagination.pcss | 2 +- css/src/styles.pcss | 2 + deploy-docker-hub.sh | 4 +- .../src/main/kotlin/usecases/NoteService.kt | 12 ++- .../main/kotlin/notes/NoteRepositoryImpl.kt | 2 +- 15 files changed, 234 insertions(+), 51 deletions(-) create mode 100644 app/src/main/kotlin/views/components/DeletedNoteTable.kt create mode 100644 app/src/main/kotlin/views/components/NoteTable.kt create mode 100644 css/src/navbar.pcss create mode 100644 css/src/note-table.pcss diff --git a/app/src/main/kotlin/controllers/NoteController.kt b/app/src/main/kotlin/controllers/NoteController.kt index a755656..d81b16d 100644 --- a/app/src/main/kotlin/controllers/NoteController.kt +++ b/app/src/main/kotlin/controllers/NoteController.kt @@ -80,7 +80,9 @@ class NoteController( val html = when (it) { MissingMeta -> view.noteEditor(jwtPayload, error = "Missing note metadata", textarea = markdownForm) InvalidMeta -> view.noteEditor(jwtPayload, error = "Invalid note metadata", textarea = markdownForm) - is ValidationError -> view.noteEditor(jwtPayload, validationErrors = it.validationErrors, textarea = markdownForm) + is ValidationError -> view.noteEditor(jwtPayload, + validationErrors = it.validationErrors, + textarea = markdownForm) } Response(BAD_REQUEST).html(html) }, @@ -90,6 +92,26 @@ class NoteController( ) } + fun trash(request: Request, jwtPayload: JwtPayload): Response { + val currentPage = request.query("page")?.toIntOrNull()?.let(::abs) ?: 1 + val tag = request.query("tag") + val (pages, notes) = noteService.paginatedNotes(jwtPayload.userId, currentPage, tag = tag, deleted = true) + return Response(OK).html(view.trash(jwtPayload, notes, currentPage, pages)) + } + + fun deleted(request: Request, jwtPayload: JwtPayload): Response { + val uuid = request.uuidPath() ?: return Response(NOT_FOUND) + return if (request.form("delete") != null) + if (noteService.delete(jwtPayload.userId, uuid)) + Response.redirect("/notes/trash") + else + Response(NOT_FOUND) + else if (noteService.restore(jwtPayload.userId, uuid)) + Response.redirect("/notes/$uuid") + else + Response(NOT_FOUND) + } + private fun Request.uuidPath(): UUID? { val uuidPath = path("uuid")!! return try { diff --git a/app/src/main/kotlin/routes/Router.kt b/app/src/main/kotlin/routes/Router.kt index 9a81d2a..9e33d75 100644 --- a/app/src/main/kotlin/routes/Router.kt +++ b/app/src/main/kotlin/routes/Router.kt @@ -48,10 +48,12 @@ class Router( "/notes" bind GET to { protected(it, noteController::list) }, "/notes/new" bind GET to { protected(it, noteController::new) }, "/notes/new" bind POST to { protected(it, noteController::new) }, + "/notes/trash" bind GET to { protected(it, noteController::trash) }, "/notes/{uuid}" bind GET to { protected(it, noteController::note) }, "/notes/{uuid}" bind POST to { protected(it, noteController::note) }, "/notes/{uuid}/edit" bind GET to { protected(it, noteController::edit) }, "/notes/{uuid}/edit" bind POST to { protected(it, noteController::edit) }, + "/notes/deleted/{uuid}" bind POST to { protected(it, noteController::deleted) }, ) val routes = routes( diff --git a/app/src/main/kotlin/views/NoteView.kt b/app/src/main/kotlin/views/NoteView.kt index 3c0aa71..75d5cff 100644 --- a/app/src/main/kotlin/views/NoteView.kt +++ b/app/src/main/kotlin/views/NoteView.kt @@ -1,9 +1,7 @@ package be.simplenotes.app.views import be.simplenotes.app.utils.StaticFileResolver -import be.simplenotes.app.views.components.Alert -import be.simplenotes.app.views.components.alert -import be.simplenotes.app.views.components.submitButton +import be.simplenotes.app.views.components.* import be.simplenotes.domain.model.PersistedNote import be.simplenotes.domain.model.PersistedNoteMetadata import be.simplenotes.domain.security.JwtPayload @@ -53,41 +51,57 @@ class NoteView(staticFileResolver: StaticFileResolver) : View(staticFileResolver notes: List, currentPage: Int, numberOfPages: Int, - tag: String? - ) = - renderPage(title = "Notes", jwtPayload = jwtPayload) { - div("container mx-auto p-4") { - div("flex justify-between mb-4") { - h1("text-2xl underline") { +"Notes" } + tag: String?, + ) = renderPage(title = "Notes", jwtPayload = jwtPayload) { + div("container mx-auto p-4") { + div("flex justify-between mb-4") { + h1("text-2xl underline") { +"Notes" } + span { + a( + href = "/notes/trash", + classes = "underline font-semibold" + ) { +"Trash" } a( href = "/notes/new", - classes = "btn btn-green" + classes = "ml-2 btn btn-green" ) { +"New" } } - if (notes.isNotEmpty()) { - ul { - notes.forEach { (title, tags, _, uuid) -> - li("flex justify-between") { - a(classes = "text-blue-200 text-xl hover:underline", href = "/notes/$uuid") { - +title - } - span("space-x-2") { - tags.forEach { - a(href = "?tag=$it", classes = "tag") { - +"#$it" - } - } - } - } - } - } - if (numberOfPages > 1) - pagination(currentPage, numberOfPages, tag) - } else - span { +"No notes yet" } // FIXME if too far in pagination, it it displayed } + if (notes.isNotEmpty()) + noteTable(notes) + else + span { + if (numberOfPages > 1) +"You went too far" + else +"No notes yet" + } + + if (numberOfPages > 1) pagination(currentPage, numberOfPages, tag) } + } + + fun trash( + jwtPayload: JwtPayload, + notes: List, + currentPage: Int, + numberOfPages: Int + ) = renderPage(title = "Notes", jwtPayload = jwtPayload) { + div("container mx-auto p-4") { + div("flex justify-between mb-4") { + h1("text-2xl underline") { +"Deleted notes" } + } + if (notes.isNotEmpty()) + deletedNoteTable(notes) + else + span { + if (numberOfPages > 1) +"You went too far" + else +"No deleted notes" + } + + if (numberOfPages > 1) pagination(currentPage, numberOfPages, null) + } + } + private fun DIV.pagination(currentPage: Int, numberOfPages: Int, tag: String?) { val links = mutableListOf>() diff --git a/app/src/main/kotlin/views/View.kt b/app/src/main/kotlin/views/View.kt index d00d61a..a50e806 100644 --- a/app/src/main/kotlin/views/View.kt +++ b/app/src/main/kotlin/views/View.kt @@ -14,7 +14,7 @@ abstract class View(private val staticFileResolver: StaticFileResolver) { title: String, description: String? = null, jwtPayload: JwtPayload?, - body: BODY.() -> Unit = {}, + body: MAIN.() -> Unit = {}, ) = buildString { appendLine("") appendHTML().html { @@ -29,7 +29,7 @@ abstract class View(private val staticFileResolver: StaticFileResolver) { } body("bg-gray-900 text-white") { navbar(jwtPayload) - main { this@body.body() } + main { body() } } } } diff --git a/app/src/main/kotlin/views/components/DeletedNoteTable.kt b/app/src/main/kotlin/views/components/DeletedNoteTable.kt new file mode 100644 index 0000000..be9300e --- /dev/null +++ b/app/src/main/kotlin/views/components/DeletedNoteTable.kt @@ -0,0 +1,51 @@ +package be.simplenotes.app.views.components + +import be.simplenotes.domain.model.PersistedNoteMetadata +import kotlinx.html.* +import kotlinx.html.ButtonType.submit +import kotlinx.html.FormMethod.post +import kotlinx.html.ThScope.col +import org.http4k.core.Method + +fun FlowContent.deletedNoteTable(notes: List) = div("overflow-x-auto") { + table { + id = "notes" + thead { + tr { + th(col, "w-1/4") { +"Title" } + th(col, "w-1/4") { +"Updated" } + th(col, "w-1/4") { +"Tags" } + th(col, "w-1/4") { +"Restore" } + } + } + tbody { + notes.forEach { (title, tags, updatedAt, uuid) -> + tr { + td { +title } + td("text-center") { +updatedAt.toString() } // TODO: x time ago + td { tags(tags) } + td("text-center") { + form(classes = "inline", method = post, action = "/notes/deleted/$uuid") { + button(classes = "btn btn-red", type = submit, name = "delete") { + +"Delete permanently" + } + button(classes = "ml-2 btn btn-green", type = submit, name = "restore") { + +"Restore" + } + } + } + } + } + } + } +} + +private fun FlowContent.tags(tags: List) { + ul("inline flex flex-wrap justify-center") { + tags.forEach { tag -> + li("mx-2 my-1") { + span("tag") { +"#$tag" } + } + } + } +} diff --git a/app/src/main/kotlin/views/components/Navbar.kt b/app/src/main/kotlin/views/components/Navbar.kt index 8962994..0497926 100644 --- a/app/src/main/kotlin/views/components/Navbar.kt +++ b/app/src/main/kotlin/views/components/Navbar.kt @@ -4,9 +4,14 @@ import be.simplenotes.domain.security.JwtPayload import kotlinx.html.* fun BODY.navbar(jwtPayload: JwtPayload?) { - nav("nav bg-teal-700 shadow-md flex items-center justify-between px-4") { - a(href = "/", classes = "text-2xl text-gray-100 font-bold") { +"SimpleNotes" } + nav { + id = "navbar" + a("/") { + id = "home" + +"SimpleNotes" + } ul("space-x-2") { + id = "navigation" if (jwtPayload != null) { val links = listOf( "/notes" to "Notes", diff --git a/app/src/main/kotlin/views/components/NoteTable.kt b/app/src/main/kotlin/views/components/NoteTable.kt new file mode 100644 index 0000000..32ff42e --- /dev/null +++ b/app/src/main/kotlin/views/components/NoteTable.kt @@ -0,0 +1,39 @@ +package be.simplenotes.app.views.components + +import be.simplenotes.domain.model.PersistedNoteMetadata +import kotlinx.html.* +import kotlinx.html.ThScope.col + +fun FlowContent.noteTable(notes: List) = div("overflow-x-auto") { + table { + id = "notes" + thead { + tr { + th(col, "w-1/2") { +"Title" } + th(col, "w-1/4") { +"Updated" } + th(col, "w-1/4") { +"Tags" } + } + } + tbody { + notes.forEach { (title, tags, updatedAt, uuid) -> + tr { + td { + a(classes = "text-blue-200 font-semibold underline", href = "/notes/$uuid") { +title } + } + td("text-center") { +updatedAt.toString() } // TODO: x time ago + td { tags(tags) } + } + } + } + } +} + +private fun FlowContent.tags(tags: List) { + ul("inline flex flex-wrap justify-center") { + tags.forEach { tag -> + li("mx-2 my-1") { + a(href = "?tag=$tag", classes = "tag") { +"#$tag" } + } + } + } +} diff --git a/css/src/navbar.pcss b/css/src/navbar.pcss new file mode 100644 index 0000000..d975bb6 --- /dev/null +++ b/css/src/navbar.pcss @@ -0,0 +1,33 @@ +#navbar { + height: 96px; + @apply bg-teal-700 shadow-md flex flex-col items-center justify-center px-4; + + #home { + @apply text-2xl text-gray-100 font-bold; + } + + #navigation { + @apply my-2; + } +} + +@screen sm { + #navbar { + height: 64px; + @apply flex-row justify-between; + + #navigation { + @apply my-0; + } + } +} + +.centered { + min-height: calc(100vh - 96px); +} + +@screen sm { + .centered { + min-height: calc(100vh - 64px); + } +} diff --git a/css/src/note-table.pcss b/css/src/note-table.pcss new file mode 100644 index 0000000..5e96f6d --- /dev/null +++ b/css/src/note-table.pcss @@ -0,0 +1,17 @@ +table#notes { + @apply table-auto w-full border-collapse border-2 border-gray-700; + + thead th { + @apply px-4 py-2; + } + + tbody { + tr:nth-child(even) { + @apply bg-gray-800; + } + + td { + @apply border border-gray-700 py-3 px-4; + } + } +} diff --git a/css/src/other.pcss b/css/src/other.pcss index 220da3f..f9a55ea 100644 --- a/css/src/other.pcss +++ b/css/src/other.pcss @@ -1,11 +1,3 @@ -.nav { - height: 64px; -} - -.centered { - min-height: calc(100vh - 64px); -} - .tag { @apply italic font-semibold text-sm bg-teal-500 text-gray-900 rounded-full py-1 px-2 align-middle; diff --git a/css/src/pagination.pcss b/css/src/pagination.pcss index d17cdcf..a8f5d36 100644 --- a/css/src/pagination.pcss +++ b/css/src/pagination.pcss @@ -20,7 +20,7 @@ nav.pages { @apply bg-gray-700; } - &:active { + &.active { @apply bg-teal-800 border-gray-700 text-white; &:hover { diff --git a/css/src/styles.pcss b/css/src/styles.pcss index 35ccd7a..77a2cec 100644 --- a/css/src/styles.pcss +++ b/css/src/styles.pcss @@ -8,6 +8,8 @@ @import "./note.pcss"; @import "./pagination.pcss"; @import "./other.pcss"; +@import "./navbar.pcss"; +@import "./note-table.pcss"; /*noinspection CssUnknownTarget*/ @import "tailwindcss/utilities"; diff --git a/deploy-docker-hub.sh b/deploy-docker-hub.sh index 818c5a9..a4f41b2 100755 --- a/deploy-docker-hub.sh +++ b/deploy-docker-hub.sh @@ -4,5 +4,5 @@ rm app/src/main/resources/css-manifest.json rm app/src/main/resources/static/styles* yarn --cwd css run css-purge \ - && docker build -t hubv/simplenotes . \ - && docker push hubv/simplenotes:latest + && docker build -t hubv/simplenotes:dev . \ + && docker push hubv/simplenotes:dev diff --git a/domain/src/main/kotlin/usecases/NoteService.kt b/domain/src/main/kotlin/usecases/NoteService.kt index 5ff2fca..937f014 100644 --- a/domain/src/main/kotlin/usecases/NoteService.kt +++ b/domain/src/main/kotlin/usecases/NoteService.kt @@ -29,11 +29,17 @@ class NoteService( .map { Note(it.metadata, markdown = markdownText, html = it.html) } .map { noteRepository.update(userId, uuid, it) } - fun paginatedNotes(userId: Int, page: Int, itemsPerPage: Int = 20, tag: String? = null): PaginatedNotes { - val count = noteRepository.count(userId, tag) + fun paginatedNotes( + userId: Int, + page: Int, + itemsPerPage: Int = 20, + tag: String? = null, + deleted: Boolean = false + ): PaginatedNotes { + val count = noteRepository.count(userId, tag, deleted) val offset = (page - 1) * itemsPerPage val numberOfPages = (count / itemsPerPage) + 1 - val notes = if (count == 0) emptyList() else noteRepository.findAll(userId, itemsPerPage, offset, tag) + val notes = if (count == 0) emptyList() else noteRepository.findAll(userId, itemsPerPage, offset, tag, deleted) return PaginatedNotes(numberOfPages, notes) } diff --git a/persistance/src/main/kotlin/notes/NoteRepositoryImpl.kt b/persistance/src/main/kotlin/notes/NoteRepositoryImpl.kt index aea00d5..e50a781 100644 --- a/persistance/src/main/kotlin/notes/NoteRepositoryImpl.kt +++ b/persistance/src/main/kotlin/notes/NoteRepositoryImpl.kt @@ -164,7 +164,7 @@ internal class NoteRepositoryImpl(private val db: Database) : NoteRepository { if (tag == null) return db.notes.count { (it.userId eq userId) and (Notes.deleted eq deleted) } return db.sequenceOf(Tags).count { - (it.name eq tag) and (it.note.userId eq userId) and (Notes.deleted eq deleted) + (it.name eq tag) and (it.note.userId eq userId) and (it.note.deleted eq deleted) } } }