diff --git a/app/src/main/kotlin/controllers/NoteController.kt b/app/src/main/kotlin/controllers/NoteController.kt
index f2a3940..ce6d465 100644
--- a/app/src/main/kotlin/controllers/NoteController.kt
+++ b/app/src/main/kotlin/controllers/NoteController.kt
@@ -2,12 +2,14 @@ package be.simplenotes.app.controllers
import be.simplenotes.app.extensions.html
import be.simplenotes.app.extensions.redirect
+import be.simplenotes.app.utils.parseSearchTerms
import be.simplenotes.app.views.NoteView
import be.simplenotes.domain.security.JwtPayload
import be.simplenotes.domain.usecases.NoteService
import be.simplenotes.domain.usecases.markdown.InvalidMeta
import be.simplenotes.domain.usecases.markdown.MissingMeta
import be.simplenotes.domain.usecases.markdown.ValidationError
+import be.simplenotes.search.SearchTerms
import org.http4k.core.Method
import org.http4k.core.Request
import org.http4k.core.Response
@@ -34,7 +36,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)
},
@@ -52,6 +56,13 @@ class NoteController(
return Response(OK).html(view.notes(jwtPayload, notes, currentPage, pages, deletedCount, tag = tag))
}
+ fun search(request: Request, jwtPayload: JwtPayload): Response {
+ val terms = request.searchTerms()
+ val notes = noteService.search(jwtPayload.userId, terms)
+ val deletedCount = noteService.countDeleted(jwtPayload.userId)
+ return Response(OK).html(view.search(jwtPayload, notes, deletedCount))
+ }
+
fun note(request: Request, jwtPayload: JwtPayload): Response {
val noteUuid = request.uuidPath() ?: return Response(NOT_FOUND)
@@ -121,4 +132,6 @@ class NoteController(
null
}
}
+
+ private fun Request.searchTerms(): SearchTerms = parseSearchTerms(form("search") ?: "")
}
diff --git a/app/src/main/kotlin/routes/Router.kt b/app/src/main/kotlin/routes/Router.kt
index 54a002a..a96db09 100644
--- a/app/src/main/kotlin/routes/Router.kt
+++ b/app/src/main/kotlin/routes/Router.kt
@@ -48,6 +48,7 @@ class Router(
"/settings" bind POST to { protected(it, settingsController::settings) },
"/export" bind POST to { protected(it, settingsController::export) },
"/notes" bind GET to { protected(it, noteController::list) },
+ "/notes" bind POST to { protected(it, noteController::search) },
"/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) },
diff --git a/app/src/main/kotlin/utils/SearchTermsParser.kt b/app/src/main/kotlin/utils/SearchTermsParser.kt
new file mode 100644
index 0000000..0062844
--- /dev/null
+++ b/app/src/main/kotlin/utils/SearchTermsParser.kt
@@ -0,0 +1,33 @@
+package be.simplenotes.app.utils
+
+import be.simplenotes.search.SearchTerms
+
+private val titleRe = """title:['"](?
.*?)['"]""".toRegex()
+private val outerTitleRe = """(?title:['"].*?['"])""".toRegex()
+
+private val tagRe = """tag:['"](?.*?)['"]""".toRegex()
+private val outerTagRe = """(?tag:['"].*?['"])""".toRegex()
+
+fun parseSearchTerms(input: String): SearchTerms {
+ val title: String? = titleRe.find(input)?.groups?.get("title")?.value
+ val tag: String? = tagRe.find(input)?.groups?.get("tag")?.value
+ var c: String = input
+
+ if (title != null) {
+ val titleGroup = outerTitleRe.find(input)?.groups?.get("title")?.value
+ titleGroup?.let { c = c.replace(it, "") }
+ }
+
+ if (tag != null) {
+ val tagGroup = outerTagRe.find(input)?.groups?.get("tag")?.value
+ tagGroup?.let { c = c.replace(it, "") }
+ }
+
+ val content = c.trim().ifEmpty { null }
+
+ return SearchTerms(
+ title = title,
+ tag = tag,
+ content = content
+ )
+}
diff --git a/app/src/main/kotlin/views/NoteView.kt b/app/src/main/kotlin/views/NoteView.kt
index c7d02a1..6e5a6b1 100644
--- a/app/src/main/kotlin/views/NoteView.kt
+++ b/app/src/main/kotlin/views/NoteView.kt
@@ -55,20 +55,7 @@ class NoteView(staticFileResolver: StaticFileResolver) : View(staticFileResolver
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 ($numberOfDeletedNotes)" }
- a(
- href = "/notes/new",
- classes = "ml-2 btn btn-green"
- ) { +"New" }
- }
-
- }
+ noteListHeader(numberOfDeletedNotes)
if (notes.isNotEmpty())
noteTable(notes)
else
@@ -81,11 +68,22 @@ class NoteView(staticFileResolver: StaticFileResolver) : View(staticFileResolver
}
}
+ fun search(
+ jwtPayload: JwtPayload,
+ notes: List,
+ numberOfDeletedNotes: Int,
+ ) = renderPage("Notes", jwtPayload = jwtPayload) {
+ div("container mx-auto p-4") {
+ noteListHeader(numberOfDeletedNotes)
+ noteTable(notes)
+ }
+ }
+
fun trash(
jwtPayload: JwtPayload,
notes: List,
currentPage: Int,
- numberOfPages: Int
+ numberOfPages: Int,
) = renderPage(title = "Notes", jwtPayload = jwtPayload) {
div("container mx-auto p-4") {
div("flex justify-between mb-4") {
diff --git a/app/src/main/kotlin/views/components/NoteListHeader.kt b/app/src/main/kotlin/views/components/NoteListHeader.kt
new file mode 100644
index 0000000..a315060
--- /dev/null
+++ b/app/src/main/kotlin/views/components/NoteListHeader.kt
@@ -0,0 +1,19 @@
+package be.simplenotes.app.views.components
+
+import kotlinx.html.*
+
+fun DIV.noteListHeader(numberOfDeletedNotes: Int) {
+ div("flex justify-between mb-4") {
+ h1("text-2xl underline") { +"Notes" }
+ span {
+ a(
+ href = "/notes/trash",
+ classes = "underline font-semibold"
+ ) { +"Trash ($numberOfDeletedNotes)" }
+ a(
+ href = "/notes/new",
+ classes = "ml-2 btn btn-green"
+ ) { +"New" }
+ }
+ }
+}
diff --git a/app/src/test/kotlin/utils/SearchTermsParserKtTest.kt b/app/src/test/kotlin/utils/SearchTermsParserKtTest.kt
new file mode 100644
index 0000000..39f996a
--- /dev/null
+++ b/app/src/test/kotlin/utils/SearchTermsParserKtTest.kt
@@ -0,0 +1,39 @@
+package be.simplenotes.app.utils
+
+import be.simplenotes.search.SearchTerms
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.MethodSource
+import java.util.stream.Stream
+
+internal class SearchTermsParserKtTest {
+
+ private fun createResult(
+ input: String,
+ title: String? = null,
+ tag: String? = null,
+ content: String? = null,
+ ): Pair = input to SearchTerms(title, tag, content)
+
+ @Suppress("Unused")
+ private fun results() = Stream.of(
+ createResult("title:'example'", title = "example"),
+ createResult("title:'example with words'", title = "example with words"),
+ createResult("title:'example with words'", title = "example with words"),
+ createResult("""title:"double quotes"""", title = "double quotes"),
+ createResult("title:'example' something else", title = "example", content = "something else"),
+ createResult("tag:'example'", tag = "example"),
+ createResult("tag:'example' title:'other'", title = "other", tag = "example"),
+ createResult("blah blah tag:'example' title:'other'", title = "other", tag = "example", content = "blah blah"),
+ createResult("tag:'example' middle title:'other'", title = "other", tag = "example", content = "middle"),
+ createResult("tag:'example' title:'other' end", title = "other", tag = "example", content = "end"),
+ createResult("tag:'example abc' title:'other with words' this is the end ", title = "other with words", tag = "example abc", content = "this is the end"),
+ )
+
+ @ParameterizedTest
+ @MethodSource("results")
+ fun `valid search parser`(case: Pair) {
+ assertThat(parseSearchTerms(case.first)).isEqualTo(case.second)
+ }
+
+}
diff --git a/domain/src/main/kotlin/usecases/NoteService.kt b/domain/src/main/kotlin/usecases/NoteService.kt
index d28d9ef..d7fdabd 100644
--- a/domain/src/main/kotlin/usecases/NoteService.kt
+++ b/domain/src/main/kotlin/usecases/NoteService.kt
@@ -11,6 +11,7 @@ import be.simplenotes.domain.usecases.markdown.MarkdownParsingError
import be.simplenotes.domain.usecases.repositories.NoteRepository
import be.simplenotes.domain.usecases.repositories.UserRepository
import be.simplenotes.search.NoteSearcher
+import be.simplenotes.search.SearchTerms
import java.util.*
class NoteService(
@@ -83,6 +84,8 @@ class NoteService(
searcher.indexNotes(id, notes)
}
}
+
+ fun search(userId: Int, searchTerms: SearchTerms) = searcher.search(userId, searchTerms)
}
data class PaginatedNotes(val pages: Int, val notes: List)