diff --git a/app/src/main/kotlin/controllers/NoteController.kt b/app/src/main/kotlin/controllers/NoteController.kt index e9ad5dc..5bc2f1c 100644 --- a/app/src/main/kotlin/controllers/NoteController.kt +++ b/app/src/main/kotlin/controllers/NoteController.kt @@ -46,8 +46,9 @@ class NoteController( fun list(request: Request, jwtPayload: JwtPayload): Response { val currentPage = request.query("page")?.toIntOrNull()?.let(::abs) ?: 1 - val (pages, notes) = noteService.paginatedNotes(jwtPayload.userId, currentPage) - return Response(OK).html(view.notes(jwtPayload, notes, currentPage, pages)) + val tag = request.query("tag") + 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 { diff --git a/app/src/main/kotlin/views/NoteView.kt b/app/src/main/kotlin/views/NoteView.kt index b82a9a7..3c0aa71 100644 --- a/app/src/main/kotlin/views/NoteView.kt +++ b/app/src/main/kotlin/views/NoteView.kt @@ -48,7 +48,13 @@ class NoteView(staticFileResolver: StaticFileResolver) : View(staticFileResolver } } - fun notes(jwtPayload: JwtPayload, notes: List, currentPage: Int, numberOfPages: Int) = + fun notes( + jwtPayload: JwtPayload, + 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") { @@ -66,25 +72,29 @@ class NoteView(staticFileResolver: StaticFileResolver) : View(staticFileResolver a(classes = "text-blue-200 text-xl hover:underline", href = "/notes/$uuid") { +title } - span { + span("space-x-2") { tags.forEach { - span("tag ml-2") { +"#$it" } + a(href = "?tag=$it", classes = "tag") { + +"#$it" + } } } } } } if (numberOfPages > 1) - pagination(currentPage, numberOfPages) + pagination(currentPage, numberOfPages, tag) } else 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>() // 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}" nav("pages") { @@ -98,9 +108,11 @@ class NoteView(staticFileResolver: StaticFileResolver) : View(staticFileResolver div("container mx-auto p-4") { div("flex items-center justify-between mb-4") { h1("text-3xl fond-bold underline") { +note.meta.title } - span { + span("space-x-2") { note.meta.tags.forEach { - span("tag ml-2") { +"#$it" } + a(href = "/notes?tag=$it", classes = "tag") { + +"#$it" + } } } } diff --git a/css/src/other.pcss b/css/src/other.pcss index e60fbbb..220da3f 100644 --- a/css/src/other.pcss +++ b/css/src/other.pcss @@ -8,4 +8,12 @@ .tag { @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; + } } diff --git a/domain/src/main/kotlin/usecases/NoteService.kt b/domain/src/main/kotlin/usecases/NoteService.kt index 58fbd0a..3799c68 100644 --- a/domain/src/main/kotlin/usecases/NoteService.kt +++ b/domain/src/main/kotlin/usecases/NoteService.kt @@ -29,11 +29,11 @@ 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): PaginatedNotes { - val count = noteRepository.count(userId) + fun paginatedNotes(userId: Int, page: Int, itemsPerPage: Int = 20, tag: String? = null): PaginatedNotes { + val count = noteRepository.count(userId, tag) val offset = (page - 1) * itemsPerPage 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) } diff --git a/domain/src/main/kotlin/usecases/repositories/NoteRepository.kt b/domain/src/main/kotlin/usecases/repositories/NoteRepository.kt index 734b045..6ddefc4 100644 --- a/domain/src/main/kotlin/usecases/repositories/NoteRepository.kt +++ b/domain/src/main/kotlin/usecases/repositories/NoteRepository.kt @@ -6,12 +6,12 @@ import be.simplenotes.domain.model.PersistedNoteMetadata import java.util.* interface NoteRepository { - fun findAll(userId: Int, limit: Int = 20, offset: Int = 0): List + fun findAll(userId: Int, limit: Int = 20, offset: Int = 0, tag: String? = null): List fun exists(userId: Int, uuid: UUID): Boolean fun create(userId: Int, note: Note): PersistedNote fun find(userId: Int, uuid: UUID): PersistedNote? fun update(userId: Int, uuid: UUID, note: Note): PersistedNote? fun delete(userId: Int, uuid: UUID): Boolean fun getTags(userId: Int): List - fun count(userId: Int): Int + fun count(userId: Int, tag: String? = null): Int } diff --git a/domain/src/main/kotlin/validation/NoteValidations.kt b/domain/src/main/kotlin/validation/NoteValidations.kt index b58a3e7..ee60422 100644 --- a/domain/src/main/kotlin/validation/NoteValidations.kt +++ b/domain/src/main/kotlin/validation/NoteValidations.kt @@ -22,6 +22,9 @@ internal object NoteValidations { NoteMetadata::tags onEach { maxLength(15) addConstraint("must not be blank") { it.isNotBlank() } + addConstraint("must only contain alphanumeric characters, `-` and `_`") { + it.matches("^[a-zA-Z0-9-_]+\$".toRegex()) + } } } diff --git a/persistance/src/main/kotlin/notes/NoteRepositoryImpl.kt b/persistance/src/main/kotlin/notes/NoteRepositoryImpl.kt index 7cd6be7..d830947 100644 --- a/persistance/src/main/kotlin/notes/NoteRepositoryImpl.kt +++ b/persistance/src/main/kotlin/notes/NoteRepositoryImpl.kt @@ -4,6 +4,7 @@ import be.simplenotes.domain.model.Note import be.simplenotes.domain.model.PersistedNote import be.simplenotes.domain.model.PersistedNoteMetadata import be.simplenotes.domain.usecases.repositories.NoteRepository +import be.simplenotes.persistance.extensions.uuidBinary import me.liuwj.ktorm.database.* import me.liuwj.ktorm.dsl.* import me.liuwj.ktorm.entity.* @@ -14,13 +15,25 @@ import kotlin.collections.HashMap internal class NoteRepositoryImpl(private val db: Database) : NoteRepository { @Throws(IllegalArgumentException::class) - override fun findAll(userId: Int, limit: Int, offset: Int): List { + override fun findAll(userId: Int, limit: Int, offset: Int, tag: String?): List { require(limit > 0) { "limit should be positive" } require(offset >= 0) { "offset should not be negative" } - val notes = db.notes + val uuids1: List? = 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) } .filter { it.userId eq userId } + + if (uuids1 != null) query = query.filter { it.uuid inList uuids1 } + + val notes = query .sortedByDescending { it.updatedAt } .take(limit) .drop(offset) @@ -119,5 +132,9 @@ internal class NoteRepositoryImpl(private val db: Database) : NoteRepository { .mapColumns(isDistinct = true) { it.name } as List } - 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) } + } } diff --git a/persistance/src/main/resources/logback.xml b/persistance/src/main/resources/logback.xml index c0633b0..5024926 100644 --- a/persistance/src/main/resources/logback.xml +++ b/persistance/src/main/resources/logback.xml @@ -10,7 +10,7 @@ - + diff --git a/persistance/src/test/kotlin/notes/NoteRepositoryImplTest.kt b/persistance/src/test/kotlin/notes/NoteRepositoryImplTest.kt index ee3dd0d..9ba0e03 100644 --- a/persistance/src/test/kotlin/notes/NoteRepositoryImplTest.kt +++ b/persistance/src/test/kotlin/notes/NoteRepositoryImplTest.kt @@ -155,6 +155,27 @@ internal class NoteRepositoryImplTest { .hasSize(10) .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