Merge branch 'trash'
This commit is contained in:
commit
ab7cfd8147
@ -55,10 +55,10 @@ class NoteController(
|
|||||||
val noteUuid = request.uuidPath() ?: return Response(NOT_FOUND)
|
val noteUuid = request.uuidPath() ?: return Response(NOT_FOUND)
|
||||||
|
|
||||||
if (request.method == Method.POST && request.form("delete") != null) {
|
if (request.method == Method.POST && request.form("delete") != null) {
|
||||||
return if (noteService.delete(jwtPayload.userId, noteUuid))
|
return if (noteService.trash(jwtPayload.userId, noteUuid))
|
||||||
Response.redirect("/notes") // FIXME: flash cookie to show success ?
|
Response.redirect("/notes") // TODO: flash cookie to show success ?
|
||||||
else
|
else
|
||||||
Response(NOT_FOUND) // FIXME: show an error
|
Response(NOT_FOUND) // TODO: show an error
|
||||||
}
|
}
|
||||||
|
|
||||||
val note = noteService.find(jwtPayload.userId, noteUuid) ?: return Response(NOT_FOUND)
|
val note = noteService.find(jwtPayload.userId, noteUuid) ?: return Response(NOT_FOUND)
|
||||||
@ -80,7 +80,9 @@ class NoteController(
|
|||||||
val html = when (it) {
|
val html = when (it) {
|
||||||
MissingMeta -> view.noteEditor(jwtPayload, error = "Missing note metadata", textarea = markdownForm)
|
MissingMeta -> view.noteEditor(jwtPayload, error = "Missing note metadata", textarea = markdownForm)
|
||||||
InvalidMeta -> view.noteEditor(jwtPayload, error = "Invalid 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)
|
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? {
|
private fun Request.uuidPath(): UUID? {
|
||||||
val uuidPath = path("uuid")!!
|
val uuidPath = path("uuid")!!
|
||||||
return try {
|
return try {
|
||||||
|
|||||||
@ -48,10 +48,12 @@ class Router(
|
|||||||
"/notes" bind GET to { protected(it, noteController::list) },
|
"/notes" bind GET to { protected(it, noteController::list) },
|
||||||
"/notes/new" bind GET to { protected(it, noteController::new) },
|
"/notes/new" bind GET to { protected(it, noteController::new) },
|
||||||
"/notes/new" bind POST 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 GET to { protected(it, noteController::note) },
|
||||||
"/notes/{uuid}" bind POST 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 GET to { protected(it, noteController::edit) },
|
||||||
"/notes/{uuid}/edit" bind POST 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(
|
val routes = routes(
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
package be.simplenotes.app.views
|
package be.simplenotes.app.views
|
||||||
|
|
||||||
import be.simplenotes.app.utils.StaticFileResolver
|
import be.simplenotes.app.utils.StaticFileResolver
|
||||||
import be.simplenotes.app.views.components.Alert
|
import be.simplenotes.app.views.components.*
|
||||||
import be.simplenotes.app.views.components.alert
|
|
||||||
import be.simplenotes.app.views.components.submitButton
|
|
||||||
import be.simplenotes.domain.model.PersistedNote
|
import be.simplenotes.domain.model.PersistedNote
|
||||||
import be.simplenotes.domain.model.PersistedNoteMetadata
|
import be.simplenotes.domain.model.PersistedNoteMetadata
|
||||||
import be.simplenotes.domain.security.JwtPayload
|
import be.simplenotes.domain.security.JwtPayload
|
||||||
@ -53,41 +51,57 @@ class NoteView(staticFileResolver: StaticFileResolver) : View(staticFileResolver
|
|||||||
notes: List<PersistedNoteMetadata>,
|
notes: List<PersistedNoteMetadata>,
|
||||||
currentPage: Int,
|
currentPage: Int,
|
||||||
numberOfPages: Int,
|
numberOfPages: Int,
|
||||||
tag: String?
|
tag: String?,
|
||||||
) =
|
) = renderPage(title = "Notes", jwtPayload = jwtPayload) {
|
||||||
renderPage(title = "Notes", jwtPayload = jwtPayload) {
|
div("container mx-auto p-4") {
|
||||||
div("container mx-auto p-4") {
|
div("flex justify-between mb-4") {
|
||||||
div("flex justify-between mb-4") {
|
h1("text-2xl underline") { +"Notes" }
|
||||||
h1("text-2xl underline") { +"Notes" }
|
span {
|
||||||
|
a(
|
||||||
|
href = "/notes/trash",
|
||||||
|
classes = "underline font-semibold"
|
||||||
|
) { +"Trash" }
|
||||||
a(
|
a(
|
||||||
href = "/notes/new",
|
href = "/notes/new",
|
||||||
classes = "btn btn-green"
|
classes = "ml-2 btn btn-green"
|
||||||
) { +"New" }
|
) { +"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<PersistedNoteMetadata>,
|
||||||
|
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?) {
|
private fun DIV.pagination(currentPage: Int, numberOfPages: Int, tag: String?) {
|
||||||
val links = mutableListOf<Pair<String, String>>()
|
val links = mutableListOf<Pair<String, String>>()
|
||||||
|
|||||||
@ -14,7 +14,7 @@ abstract class View(private val staticFileResolver: StaticFileResolver) {
|
|||||||
title: String,
|
title: String,
|
||||||
description: String? = null,
|
description: String? = null,
|
||||||
jwtPayload: JwtPayload?,
|
jwtPayload: JwtPayload?,
|
||||||
body: BODY.() -> Unit = {},
|
body: MAIN.() -> Unit = {},
|
||||||
) = buildString {
|
) = buildString {
|
||||||
appendLine("<!DOCTYPE html>")
|
appendLine("<!DOCTYPE html>")
|
||||||
appendHTML().html {
|
appendHTML().html {
|
||||||
@ -29,7 +29,7 @@ abstract class View(private val staticFileResolver: StaticFileResolver) {
|
|||||||
}
|
}
|
||||||
body("bg-gray-900 text-white") {
|
body("bg-gray-900 text-white") {
|
||||||
navbar(jwtPayload)
|
navbar(jwtPayload)
|
||||||
main { this@body.body() }
|
main { body() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
51
app/src/main/kotlin/views/components/DeletedNoteTable.kt
Normal file
51
app/src/main/kotlin/views/components/DeletedNoteTable.kt
Normal file
@ -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<PersistedNoteMetadata>) = 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<String>) {
|
||||||
|
ul("inline flex flex-wrap justify-center") {
|
||||||
|
tags.forEach { tag ->
|
||||||
|
li("mx-2 my-1") {
|
||||||
|
span("tag") { +"#$tag" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,9 +4,14 @@ import be.simplenotes.domain.security.JwtPayload
|
|||||||
import kotlinx.html.*
|
import kotlinx.html.*
|
||||||
|
|
||||||
fun BODY.navbar(jwtPayload: JwtPayload?) {
|
fun BODY.navbar(jwtPayload: JwtPayload?) {
|
||||||
nav("nav bg-teal-700 shadow-md flex items-center justify-between px-4") {
|
nav {
|
||||||
a(href = "/", classes = "text-2xl text-gray-100 font-bold") { +"SimpleNotes" }
|
id = "navbar"
|
||||||
|
a("/") {
|
||||||
|
id = "home"
|
||||||
|
+"SimpleNotes"
|
||||||
|
}
|
||||||
ul("space-x-2") {
|
ul("space-x-2") {
|
||||||
|
id = "navigation"
|
||||||
if (jwtPayload != null) {
|
if (jwtPayload != null) {
|
||||||
val links = listOf(
|
val links = listOf(
|
||||||
"/notes" to "Notes",
|
"/notes" to "Notes",
|
||||||
|
|||||||
39
app/src/main/kotlin/views/components/NoteTable.kt
Normal file
39
app/src/main/kotlin/views/components/NoteTable.kt
Normal file
@ -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<PersistedNoteMetadata>) = 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<String>) {
|
||||||
|
ul("inline flex flex-wrap justify-center") {
|
||||||
|
tags.forEach { tag ->
|
||||||
|
li("mx-2 my-1") {
|
||||||
|
a(href = "?tag=$tag", classes = "tag") { +"#$tag" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
css/src/navbar.pcss
Normal file
33
css/src/navbar.pcss
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
17
css/src/note-table.pcss
Normal file
17
css/src/note-table.pcss
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,11 +1,3 @@
|
|||||||
.nav {
|
|
||||||
height: 64px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.centered {
|
|
||||||
min-height: calc(100vh - 64px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag {
|
.tag {
|
||||||
@apply italic font-semibold text-sm bg-teal-500 text-gray-900 rounded-full py-1 px-2 align-middle;
|
@apply italic font-semibold text-sm bg-teal-500 text-gray-900 rounded-full py-1 px-2 align-middle;
|
||||||
|
|
||||||
|
|||||||
@ -20,7 +20,7 @@ nav.pages {
|
|||||||
@apply bg-gray-700;
|
@apply bg-gray-700;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&.active {
|
||||||
@apply bg-teal-800 border-gray-700 text-white;
|
@apply bg-teal-800 border-gray-700 text-white;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|||||||
@ -8,6 +8,8 @@
|
|||||||
@import "./note.pcss";
|
@import "./note.pcss";
|
||||||
@import "./pagination.pcss";
|
@import "./pagination.pcss";
|
||||||
@import "./other.pcss";
|
@import "./other.pcss";
|
||||||
|
@import "./navbar.pcss";
|
||||||
|
@import "./note-table.pcss";
|
||||||
|
|
||||||
/*noinspection CssUnknownTarget*/
|
/*noinspection CssUnknownTarget*/
|
||||||
@import "tailwindcss/utilities";
|
@import "tailwindcss/utilities";
|
||||||
|
|||||||
@ -4,5 +4,5 @@ rm app/src/main/resources/css-manifest.json
|
|||||||
rm app/src/main/resources/static/styles*
|
rm app/src/main/resources/static/styles*
|
||||||
|
|
||||||
yarn --cwd css run css-purge \
|
yarn --cwd css run css-purge \
|
||||||
&& docker build -t hubv/simplenotes . \
|
&& docker build -t hubv/simplenotes:dev . \
|
||||||
&& docker push hubv/simplenotes:latest
|
&& docker push hubv/simplenotes:dev
|
||||||
|
|||||||
@ -29,16 +29,25 @@ class NoteService(
|
|||||||
.map { Note(it.metadata, markdown = markdownText, html = it.html) }
|
.map { Note(it.metadata, markdown = markdownText, html = it.html) }
|
||||||
.map { noteRepository.update(userId, uuid, it) }
|
.map { noteRepository.update(userId, uuid, it) }
|
||||||
|
|
||||||
fun paginatedNotes(userId: Int, page: Int, itemsPerPage: Int = 20, tag: String? = null): PaginatedNotes {
|
fun paginatedNotes(
|
||||||
val count = noteRepository.count(userId, tag)
|
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 offset = (page - 1) * itemsPerPage
|
||||||
val numberOfPages = (count / itemsPerPage) + 1
|
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)
|
return PaginatedNotes(numberOfPages, notes)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun find(userId: Int, uuid: UUID) = noteRepository.find(userId, uuid)
|
fun find(userId: Int, uuid: UUID) = noteRepository.find(userId, uuid)
|
||||||
fun delete(userId: Int, uuid: UUID) = noteRepository.delete(userId, uuid)
|
fun trash(userId: Int, uuid: UUID) = noteRepository.delete(userId, uuid, permanent = false)
|
||||||
|
fun restore(userId: Int, uuid: UUID) = noteRepository.restore(userId, uuid)
|
||||||
|
fun delete(userId: Int, uuid: UUID) = noteRepository.delete(userId, uuid, permanent = true)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class PaginatedNotes(val pages: Int, val notes: List<PersistedNoteMetadata>)
|
data class PaginatedNotes(val pages: Int, val notes: List<PersistedNoteMetadata>)
|
||||||
|
|||||||
@ -6,12 +6,23 @@ import be.simplenotes.domain.model.PersistedNoteMetadata
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
interface NoteRepository {
|
interface NoteRepository {
|
||||||
fun findAll(userId: Int, limit: Int = 20, offset: Int = 0, tag: String? = null): List<PersistedNoteMetadata>
|
|
||||||
|
fun findAll(
|
||||||
|
userId: Int,
|
||||||
|
limit: Int = 20,
|
||||||
|
offset: Int = 0,
|
||||||
|
tag: String? = null,
|
||||||
|
deleted: Boolean = false
|
||||||
|
): List<PersistedNoteMetadata>
|
||||||
|
|
||||||
|
fun count(userId: Int, tag: String? = null, deleted: Boolean = false): Int
|
||||||
|
fun delete(userId: Int, uuid: UUID, permanent: Boolean = false): Boolean
|
||||||
|
fun restore(userId: Int, uuid: UUID): Boolean
|
||||||
|
|
||||||
|
// These methods only access notes where `Notes.deleted = false`
|
||||||
|
fun getTags(userId: Int): List<String>
|
||||||
fun exists(userId: Int, uuid: UUID): Boolean
|
fun exists(userId: Int, uuid: UUID): Boolean
|
||||||
fun create(userId: Int, note: Note): PersistedNote
|
fun create(userId: Int, note: Note): PersistedNote
|
||||||
fun find(userId: Int, uuid: UUID): PersistedNote?
|
fun find(userId: Int, uuid: UUID): PersistedNote?
|
||||||
fun update(userId: Int, uuid: UUID, note: Note): PersistedNote?
|
fun update(userId: Int, uuid: UUID, note: Note): PersistedNote?
|
||||||
fun delete(userId: Int, uuid: UUID): Boolean
|
|
||||||
fun getTags(userId: Int): List<String>
|
|
||||||
fun count(userId: Int, tag: String? = null): Int
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,8 +4,7 @@ import be.simplenotes.domain.model.Note
|
|||||||
import be.simplenotes.domain.model.PersistedNote
|
import be.simplenotes.domain.model.PersistedNote
|
||||||
import be.simplenotes.domain.model.PersistedNoteMetadata
|
import be.simplenotes.domain.model.PersistedNoteMetadata
|
||||||
import be.simplenotes.domain.usecases.repositories.NoteRepository
|
import be.simplenotes.domain.usecases.repositories.NoteRepository
|
||||||
import be.simplenotes.persistance.extensions.uuidBinary
|
import me.liuwj.ktorm.database.Database
|
||||||
import me.liuwj.ktorm.database.*
|
|
||||||
import me.liuwj.ktorm.dsl.*
|
import me.liuwj.ktorm.dsl.*
|
||||||
import me.liuwj.ktorm.entity.*
|
import me.liuwj.ktorm.entity.*
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
@ -15,7 +14,13 @@ import kotlin.collections.HashMap
|
|||||||
internal class NoteRepositoryImpl(private val db: Database) : NoteRepository {
|
internal class NoteRepositoryImpl(private val db: Database) : NoteRepository {
|
||||||
|
|
||||||
@Throws(IllegalArgumentException::class)
|
@Throws(IllegalArgumentException::class)
|
||||||
override fun findAll(userId: Int, limit: Int, offset: Int, tag: String?): List<PersistedNoteMetadata> {
|
override fun findAll(
|
||||||
|
userId: Int,
|
||||||
|
limit: Int,
|
||||||
|
offset: Int,
|
||||||
|
tag: String?,
|
||||||
|
deleted: Boolean
|
||||||
|
): List<PersistedNoteMetadata> {
|
||||||
require(limit > 0) { "limit should be positive" }
|
require(limit > 0) { "limit should be positive" }
|
||||||
require(offset >= 0) { "offset should not be negative" }
|
require(offset >= 0) { "offset should not be negative" }
|
||||||
|
|
||||||
@ -23,13 +28,13 @@ internal class NoteRepositoryImpl(private val db: Database) : NoteRepository {
|
|||||||
db.from(Tags)
|
db.from(Tags)
|
||||||
.leftJoin(Notes, on = Notes.uuid eq Tags.noteUuid)
|
.leftJoin(Notes, on = Notes.uuid eq Tags.noteUuid)
|
||||||
.select(Notes.uuid)
|
.select(Notes.uuid)
|
||||||
.where { (Notes.userId eq userId) and (Tags.name eq tag) }
|
.where { (Notes.userId eq userId) and (Tags.name eq tag) and (Notes.deleted eq deleted) }
|
||||||
.map { it[Notes.uuid]!! }
|
.map { it[Notes.uuid]!! }
|
||||||
} else null
|
} else null
|
||||||
|
|
||||||
var query = db.notes
|
var query = db.notes
|
||||||
.filterColumns { listOf(it.uuid, it.title, it.updatedAt) }
|
.filterColumns { listOf(it.uuid, it.title, it.updatedAt) }
|
||||||
.filter { it.userId eq userId }
|
.filter { (it.userId eq userId) and (it.deleted eq deleted) }
|
||||||
|
|
||||||
if (uuids1 != null) query = query.filter { it.uuid inList uuids1 }
|
if (uuids1 != null) query = query.filter { it.uuid inList uuids1 }
|
||||||
|
|
||||||
@ -55,7 +60,7 @@ internal class NoteRepositoryImpl(private val db: Database) : NoteRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun exists(userId: Int, uuid: UUID): Boolean {
|
override fun exists(userId: Int, uuid: UUID): Boolean {
|
||||||
return db.notes.any { (it.userId eq userId) and (it.uuid eq uuid) }
|
return db.notes.any { (it.userId eq userId) and (it.uuid eq uuid) and (it.deleted eq false) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun create(userId: Int, note: Note): PersistedNote {
|
override fun create(userId: Int, note: Note): PersistedNote {
|
||||||
@ -78,17 +83,17 @@ internal class NoteRepositoryImpl(private val db: Database) : NoteRepository {
|
|||||||
return entity.toPersistedNote(note.meta.tags)
|
return entity.toPersistedNote(note.meta.tags)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
override fun find(userId: Int, uuid: UUID): PersistedNote? {
|
override fun find(userId: Int, uuid: UUID): PersistedNote? {
|
||||||
val note = db.notes
|
val note = db.notes
|
||||||
.filterColumns { it.columns - it.userId }
|
.filterColumns { it.columns - it.userId }
|
||||||
.filter { it.uuid eq uuid }
|
.filter { it.uuid eq uuid }
|
||||||
.find { it.userId eq userId }
|
.find { (it.userId eq userId) and (it.deleted eq false) }
|
||||||
?: return null
|
?: return null
|
||||||
|
|
||||||
val tags = db.tags
|
val tags = db.from(Tags)
|
||||||
.filter { it.noteUuid eq uuid }
|
.select(Tags.name)
|
||||||
.mapColumns { it.name } as List<String>
|
.where { Tags.noteUuid eq uuid }
|
||||||
|
.map { it[Tags.name]!! }
|
||||||
|
|
||||||
return note.toPersistedNote(tags)
|
return note.toPersistedNote(tags)
|
||||||
}
|
}
|
||||||
@ -96,7 +101,7 @@ internal class NoteRepositoryImpl(private val db: Database) : NoteRepository {
|
|||||||
override fun update(userId: Int, uuid: UUID, note: Note): PersistedNote? {
|
override fun update(userId: Int, uuid: UUID, note: Note): PersistedNote? {
|
||||||
db.useTransaction {
|
db.useTransaction {
|
||||||
val currentNote = db.notes
|
val currentNote = db.notes
|
||||||
.find { it.uuid eq uuid and (it.userId eq userId) }
|
.find { (it.uuid eq uuid) and (it.userId eq userId) and (it.deleted eq false) }
|
||||||
?: return null
|
?: return null
|
||||||
|
|
||||||
currentNote.title = note.meta.title
|
currentNote.title = note.meta.title
|
||||||
@ -121,20 +126,45 @@ internal class NoteRepositoryImpl(private val db: Database) : NoteRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun delete(userId: Int, uuid: UUID): Boolean = db.useTransaction {
|
override fun delete(userId: Int, uuid: UUID, permanent: Boolean): Boolean {
|
||||||
db.delete(Notes) { it.uuid eq uuid and (it.userId eq userId) } == 1
|
if (!permanent) {
|
||||||
|
return db.useTransaction {
|
||||||
|
db.update(Notes) {
|
||||||
|
it.deleted to true
|
||||||
|
it.updatedAt to LocalDateTime.now()
|
||||||
|
where { it.userId eq userId and (it.uuid eq uuid) }
|
||||||
|
}
|
||||||
|
} == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.useTransaction {
|
||||||
|
db.delete(Notes) { it.uuid eq uuid and (it.userId eq userId) } == 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
override fun restore(userId: Int, uuid: UUID): Boolean {
|
||||||
override fun getTags(userId: Int): List<String> {
|
return db.useTransaction {
|
||||||
return db.sequenceOf(Tags)
|
db.update(Notes) {
|
||||||
.filter { it.note.userId eq userId }
|
it.deleted to false
|
||||||
.mapColumns(isDistinct = true) { it.name } as List<String>
|
where {
|
||||||
|
(it.userId eq userId) and (it.uuid eq uuid)
|
||||||
|
}
|
||||||
|
} == 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun count(userId: Int, tag: String?): Int {
|
override fun getTags(userId: Int): List<String> =
|
||||||
if (tag == null) return db.notes.count { it.userId eq userId }
|
db.from(Tags)
|
||||||
return db.sequenceOf(Tags)
|
.leftJoin(Notes, on = Notes.uuid eq Tags.noteUuid)
|
||||||
.count { it.name eq tag and (it.note.userId eq userId) }
|
.selectDistinct(Tags.name)
|
||||||
|
.where { (Notes.userId eq userId) and (Notes.deleted eq false) }
|
||||||
|
.map { it[Tags.name]!! }
|
||||||
|
|
||||||
|
override fun count(userId: Int, tag: String?, deleted: Boolean): Int {
|
||||||
|
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 (it.note.deleted eq deleted)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,6 +24,7 @@ internal open class Notes(alias: String?) : Table<NoteEntity>("Notes", alias) {
|
|||||||
val html = text("html").bindTo { it.html }
|
val html = text("html").bindTo { it.html }
|
||||||
val userId = int("user_id").references(Users) { it.user }
|
val userId = int("user_id").references(Users) { it.user }
|
||||||
val updatedAt = datetime("updated_at").bindTo { it.updatedAt }
|
val updatedAt = datetime("updated_at").bindTo { it.updatedAt }
|
||||||
|
val deleted = boolean("deleted").bindTo { it.deleted }
|
||||||
val user get() = userId.referenceTable as Users
|
val user get() = userId.referenceTable as Users
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,6 +38,7 @@ internal interface NoteEntity : Entity<NoteEntity> {
|
|||||||
var markdown: String
|
var markdown: String
|
||||||
var html: String
|
var html: String
|
||||||
var updatedAt: LocalDateTime
|
var updatedAt: LocalDateTime
|
||||||
|
var deleted: Boolean
|
||||||
|
|
||||||
var user: UserEntity
|
var user: UserEntity
|
||||||
}
|
}
|
||||||
@ -52,6 +54,7 @@ internal fun Note.toEntity(uuid: UUID, userId: Int): NoteEntity {
|
|||||||
this.markdown = note.markdown
|
this.markdown = note.markdown
|
||||||
this.html = note.html
|
this.html = note.html
|
||||||
this.uuid = uuid
|
this.uuid = uuid
|
||||||
|
this.deleted = false
|
||||||
this.user["id"] = userId
|
this.user["id"] = userId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,2 @@
|
|||||||
|
alter table Notes
|
||||||
|
add column deleted bool not null default false
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
alter table Notes
|
||||||
|
add column deleted bool not null default false
|
||||||
@ -224,9 +224,6 @@ internal class NoteRepositoryImplTest {
|
|||||||
val note = createNote(user1.id, "1", listOf("a", "b"))
|
val note = createNote(user1.id, "1", listOf("a", "b"))
|
||||||
assertThat(noteRepo.delete(user1.id, note.uuid))
|
assertThat(noteRepo.delete(user1.id, note.uuid))
|
||||||
.isTrue
|
.isTrue
|
||||||
|
|
||||||
assertThat(noteRepo.delete(user1.id, note.uuid))
|
|
||||||
.isFalse
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -289,4 +286,42 @@ internal class NoteRepositoryImplTest {
|
|||||||
.isEqualToComparingOnlyGivenFields(newNote2, "meta", "markdown", "html")
|
.isEqualToComparingOnlyGivenFields(newNote2, "meta", "markdown", "html")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
inner class Trash {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `trashed noted should be restored`() {
|
||||||
|
val note1 = createNote(user1.id, "1", listOf("a", "b"))
|
||||||
|
|
||||||
|
assertThat(noteRepo.delete(user1.id, note1.uuid, permanent = false))
|
||||||
|
.isTrue
|
||||||
|
|
||||||
|
val isDeleted = db.notes
|
||||||
|
.find { it.uuid eq note1.uuid }
|
||||||
|
?.deleted
|
||||||
|
|
||||||
|
assertThat(isDeleted).`as`("Check that Notes.deleted is true").isTrue
|
||||||
|
|
||||||
|
assertThat(noteRepo.restore(user1.id, note1.uuid)).isTrue
|
||||||
|
|
||||||
|
val isDeleted2 = db.notes
|
||||||
|
.find { it.uuid eq note1.uuid }
|
||||||
|
?.deleted
|
||||||
|
|
||||||
|
assertThat(isDeleted2).`as`("Check that Notes.deleted is false after restore()").isFalse
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `permanent delete`() {
|
||||||
|
val note1 = createNote(user1.id, "1", listOf("a", "b"))
|
||||||
|
assertThat(noteRepo.delete(user1.id, note1.uuid, permanent = true))
|
||||||
|
.isTrue
|
||||||
|
|
||||||
|
assertThat(noteRepo.restore(user1.id, note1.uuid)).isFalse
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user