Merge branch 'list-by-tags'
This commit is contained in:
commit
4e2fe463e0
@ -46,8 +46,9 @@ class NoteController(
|
|||||||
|
|
||||||
fun list(request: Request, jwtPayload: JwtPayload): Response {
|
fun list(request: Request, jwtPayload: JwtPayload): Response {
|
||||||
val currentPage = request.query("page")?.toIntOrNull()?.let(::abs) ?: 1
|
val currentPage = request.query("page")?.toIntOrNull()?.let(::abs) ?: 1
|
||||||
val (pages, notes) = noteService.paginatedNotes(jwtPayload.userId, currentPage)
|
val tag = request.query("tag")
|
||||||
return Response(OK).html(view.notes(jwtPayload, notes, currentPage, pages))
|
val (pages, notes) = noteService.paginatedNotes(jwtPayload.userId, currentPage, tag = tag)
|
||||||
|
return Response(OK).html(view.notes(jwtPayload, notes, currentPage, pages, tag = tag))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun note(request: Request, jwtPayload: JwtPayload): Response {
|
fun note(request: Request, jwtPayload: JwtPayload): Response {
|
||||||
|
|||||||
@ -48,7 +48,13 @@ class NoteView(staticFileResolver: StaticFileResolver) : View(staticFileResolver
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun notes(jwtPayload: JwtPayload, notes: List<PersistedNoteMetadata>, currentPage: Int, numberOfPages: Int) =
|
fun notes(
|
||||||
|
jwtPayload: JwtPayload,
|
||||||
|
notes: List<PersistedNoteMetadata>,
|
||||||
|
currentPage: Int,
|
||||||
|
numberOfPages: Int,
|
||||||
|
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") {
|
||||||
@ -66,25 +72,29 @@ class NoteView(staticFileResolver: StaticFileResolver) : View(staticFileResolver
|
|||||||
a(classes = "text-blue-200 text-xl hover:underline", href = "/notes/$uuid") {
|
a(classes = "text-blue-200 text-xl hover:underline", href = "/notes/$uuid") {
|
||||||
+title
|
+title
|
||||||
}
|
}
|
||||||
span {
|
span("space-x-2") {
|
||||||
tags.forEach {
|
tags.forEach {
|
||||||
span("tag ml-2") { +"#$it" }
|
a(href = "?tag=$it", classes = "tag") {
|
||||||
|
+"#$it"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (numberOfPages > 1)
|
if (numberOfPages > 1)
|
||||||
pagination(currentPage, numberOfPages)
|
pagination(currentPage, numberOfPages, tag)
|
||||||
} else
|
} else
|
||||||
span { +"No notes yet" } // FIXME if too far in pagination, it it displayed
|
span { +"No notes yet" } // FIXME if too far in pagination, it it displayed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun DIV.pagination(currentPage: Int, numberOfPages: Int) {
|
private fun DIV.pagination(currentPage: Int, numberOfPages: Int, tag: String?) {
|
||||||
val links = mutableListOf<Pair<String, String>>()
|
val links = mutableListOf<Pair<String, String>>()
|
||||||
// if (currentPage > 1) links += "Previous" to "?page=${currentPage - 1}"
|
// if (currentPage > 1) links += "Previous" to "?page=${currentPage - 1}"
|
||||||
links += (1..numberOfPages).map { "$it" to "?page=$it" }
|
links += (1..numberOfPages).map { page ->
|
||||||
|
"$page" to (tag?.let { "?page=$page&tag=$it" } ?: "?page=$page")
|
||||||
|
}
|
||||||
// if (currentPage < numberOfPages) links += "Next" to "?page=${currentPage + 1}"
|
// if (currentPage < numberOfPages) links += "Next" to "?page=${currentPage + 1}"
|
||||||
|
|
||||||
nav("pages") {
|
nav("pages") {
|
||||||
@ -98,9 +108,11 @@ class NoteView(staticFileResolver: StaticFileResolver) : View(staticFileResolver
|
|||||||
div("container mx-auto p-4") {
|
div("container mx-auto p-4") {
|
||||||
div("flex items-center justify-between mb-4") {
|
div("flex items-center justify-between mb-4") {
|
||||||
h1("text-3xl fond-bold underline") { +note.meta.title }
|
h1("text-3xl fond-bold underline") { +note.meta.title }
|
||||||
span {
|
span("space-x-2") {
|
||||||
note.meta.tags.forEach {
|
note.meta.tags.forEach {
|
||||||
span("tag ml-2") { +"#$it" }
|
a(href = "/notes?tag=$it", classes = "tag") {
|
||||||
|
+"#$it"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,4 +8,12 @@
|
|||||||
|
|
||||||
.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;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
@apply outline-none shadow-outline bg-teal-800 text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
@apply bg-teal-800 text-white;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,11 +29,11 @@ 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): PaginatedNotes {
|
fun paginatedNotes(userId: Int, page: Int, itemsPerPage: Int = 20, tag: String? = null): PaginatedNotes {
|
||||||
val count = noteRepository.count(userId)
|
val count = noteRepository.count(userId, tag)
|
||||||
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)
|
val notes = if (count == 0) emptyList() else noteRepository.findAll(userId, itemsPerPage, offset, tag)
|
||||||
return PaginatedNotes(numberOfPages, notes)
|
return PaginatedNotes(numberOfPages, notes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,12 +6,12 @@ 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): List<PersistedNoteMetadata>
|
fun findAll(userId: Int, limit: Int = 20, offset: Int = 0, tag: String? = null): List<PersistedNoteMetadata>
|
||||||
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 delete(userId: Int, uuid: UUID): Boolean
|
||||||
fun getTags(userId: Int): List<String>
|
fun getTags(userId: Int): List<String>
|
||||||
fun count(userId: Int): Int
|
fun count(userId: Int, tag: String? = null): Int
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,9 @@ internal object NoteValidations {
|
|||||||
NoteMetadata::tags onEach {
|
NoteMetadata::tags onEach {
|
||||||
maxLength(15)
|
maxLength(15)
|
||||||
addConstraint("must not be blank") { it.isNotBlank() }
|
addConstraint("must not be blank") { it.isNotBlank() }
|
||||||
|
addConstraint("must only contain alphanumeric characters, `-` and `_`") {
|
||||||
|
it.matches("^[a-zA-Z0-9-_]+\$".toRegex())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +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.*
|
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.*
|
||||||
@ -14,13 +15,25 @@ 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): List<PersistedNoteMetadata> {
|
override fun findAll(userId: Int, limit: Int, offset: Int, tag: String?): 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" }
|
||||||
|
|
||||||
val notes = db.notes
|
val uuids1: List<UUID>? = if (tag != null) {
|
||||||
|
db.from(Tags)
|
||||||
|
.leftJoin(Notes, on = Notes.uuid eq Tags.noteUuid)
|
||||||
|
.select(Notes.uuid)
|
||||||
|
.where { (Notes.userId eq userId) and (Tags.name eq tag) }
|
||||||
|
.map { it[Notes.uuid]!! }
|
||||||
|
} else null
|
||||||
|
|
||||||
|
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 }
|
||||||
|
|
||||||
|
if (uuids1 != null) query = query.filter { it.uuid inList uuids1 }
|
||||||
|
|
||||||
|
val notes = query
|
||||||
.sortedByDescending { it.updatedAt }
|
.sortedByDescending { it.updatedAt }
|
||||||
.take(limit)
|
.take(limit)
|
||||||
.drop(offset)
|
.drop(offset)
|
||||||
@ -119,5 +132,9 @@ internal class NoteRepositoryImpl(private val db: Database) : NoteRepository {
|
|||||||
.mapColumns(isDistinct = true) { it.name } as List<String>
|
.mapColumns(isDistinct = true) { it.name } as List<String>
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun count(userId: Int) = db.notes.count { it.userId eq userId }
|
override fun count(userId: Int, tag: String?): Int {
|
||||||
|
if (tag == null) return db.notes.count { it.userId eq userId }
|
||||||
|
return db.sequenceOf(Tags)
|
||||||
|
.count { it.name eq tag and (it.note.userId eq userId) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
<appender-ref ref="STDOUT"/>
|
<appender-ref ref="STDOUT"/>
|
||||||
</root>
|
</root>
|
||||||
<logger name="org.eclipse.jetty" level="INFO"/>
|
<logger name="org.eclipse.jetty" level="INFO"/>
|
||||||
<logger name="me.liuwj.ktorm.database" level="INFO"/>
|
<logger name="me.liuwj.ktorm.database" level="DEBUG"/>
|
||||||
<logger name="com.zaxxer.hikari" level="INFO"/>
|
<logger name="com.zaxxer.hikari" level="INFO"/>
|
||||||
<logger name="org.flywaydb.core" level="INFO"/>
|
<logger name="org.flywaydb.core" level="INFO"/>
|
||||||
</configuration>
|
</configuration>
|
||||||
|
|||||||
@ -155,6 +155,27 @@ internal class NoteRepositoryImplTest {
|
|||||||
.hasSize(10)
|
.hasSize(10)
|
||||||
.allMatch { it.title.toInt() in 41..50 }
|
.allMatch { it.title.toInt() in 41..50 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `find all notes with tag`() {
|
||||||
|
createNote(user1.id, "1", listOf("a", "b"))
|
||||||
|
createNote(user1.id, "2")
|
||||||
|
createNote(user1.id, "3", listOf("c"))
|
||||||
|
createNote(user1.id, "4", listOf("c"))
|
||||||
|
createNote(user2.id, "5", listOf("c"))
|
||||||
|
|
||||||
|
assertThat(noteRepo.findAll(user1.id, tag = "a"))
|
||||||
|
.hasSize(1)
|
||||||
|
.first()
|
||||||
|
.hasFieldOrPropertyWithValue("title", "1")
|
||||||
|
|
||||||
|
assertThat(noteRepo.findAll(user1.id, tag = "c"))
|
||||||
|
.hasSize(2)
|
||||||
|
|
||||||
|
assertThat(noteRepo.findAll(user2.id, tag = "c"))
|
||||||
|
.hasSize(1)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user