Adds tag search.

This commit is contained in:
Stefan Schallerl 2025-02-07 16:00:59 +01:00
parent 2c2db1a42e
commit 65239fc0d2
9 changed files with 118 additions and 17 deletions

View file

@ -54,6 +54,7 @@ class FilemureApp(repository: SqliteRepository) {
server.post("/file/{extId}/delete", documentController::deleteFileAction, Role.USER) server.post("/file/{extId}/delete", documentController::deleteFileAction, Role.USER)
server.get("/search", searchController::search, Role.USER) server.get("/search", searchController::search, Role.USER)
server.get("/tags", searchController::tags, Role.USER)
server.exception(UnauthorizedResponse::class.java) { e, ctx -> server.exception(UnauthorizedResponse::class.java) { e, ctx ->
ctx.tempolin( ctx.tempolin(

View file

@ -6,7 +6,7 @@ import java.net.URLEncoder
class TemplateModifiers : Frame.Modifiers, Limbo.Modifiers, DocumentCreateForm.Modifiers, Overview.Modifiers, class TemplateModifiers : Frame.Modifiers, Limbo.Modifiers, DocumentCreateForm.Modifiers, Overview.Modifiers,
FilePreview.Modifiers, DocumentEditForm.Modifiers, FileList.Modifiers, FilePreview.Modifiers, DocumentEditForm.Modifiers, FileList.Modifiers,
net.h34t.filemure.tpl.Document.Modifiers, OverviewDocuments.Modifiers, Search.Modifiers { net.h34t.filemure.tpl.Document.Modifiers, OverviewDocuments.Modifiers, Search.Modifiers, Tags.Modifiers {
fun hashPrefix(arg: String): String { fun hashPrefix(arg: String): String {
return URLEncoder.encode(arg, Charsets.UTF_8) return URLEncoder.encode(arg, Charsets.UTF_8)

View file

@ -38,10 +38,19 @@ data class Document(
) )
@JvmInline @JvmInline
value class Tag(val value: String) { value class Tag private constructor(val value: String) {
init {
// TODO proper validation companion object {
require(value.isNotBlank()) private val validator = Regex("[a-zA-Z0-9]+")
private val splitter = Regex("\\s+")
fun of(value: String): Tag =
value.trim().let { v ->
require(v.matches(validator))
Tag(v.lowercase())
}
fun parse(text: String?) =
text?.split(splitter)?.map { of(it) } ?: emptyList()
} }
} }

View file

@ -52,7 +52,7 @@ private val tagSplitRegex = Regex("\\s+")
object TagAdapter { object TagAdapter {
fun parse(ser: String?): List<Tag> { fun parse(ser: String?): List<Tag> {
return ser?.trim()?.let { if (it.isNotBlank()) it.split(tagSplitRegex).map { Tag(it) } else emptyList() } return ser?.trim()?.let { if (it.isNotBlank()) it.split(tagSplitRegex).map { Tag.of(it) } else emptyList() }
?: emptyList() ?: emptyList()
} }

View file

@ -10,8 +10,6 @@ import net.h34t.filemure.tpl.*
import net.h34t.filemure.tpl.Document import net.h34t.filemure.tpl.Document
import java.io.File import java.io.File
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
class DocumentController(val modifiers: TemplateModifiers, val repository: SqliteRepository) { class DocumentController(val modifiers: TemplateModifiers, val repository: SqliteRepository) {
@ -117,8 +115,7 @@ class DocumentController(val modifiers: TemplateModifiers, val repository: Sqlit
?: throw BadRequestResponse("") ?: throw BadRequestResponse("")
val referenceDate = ctx.formParam("reference_date")?.let { LocalDateTime.parse(it, formDtf) } val referenceDate = ctx.formParam("reference_date")?.let { LocalDateTime.parse(it, formDtf) }
?: throw BadRequestResponse("") ?: throw BadRequestResponse("")
val tags = ctx.formParam("tags")?.split("\\s+") val tags = Tag.parse(ctx.formParam("tags"))
?: emptyList()
val description = ctx.formParam("description") val description = ctx.formParam("description")
?: throw BadRequestResponse("") ?: throw BadRequestResponse("")
val fileExtIds = ctx.formParams("file_id") val fileExtIds = ctx.formParams("file_id")
@ -128,7 +125,7 @@ class DocumentController(val modifiers: TemplateModifiers, val repository: Sqlit
session.id, session.id,
title, title,
referenceDate, referenceDate,
tags.map { Tag(it) }, tags,
description, description,
fileExtIds.map { ExtId(it) }) fileExtIds.map { ExtId(it) })

View file

@ -1,13 +1,11 @@
package net.h34t.filemure.controller package net.h34t.filemure.controller
import io.javalin.http.Context import io.javalin.http.Context
import net.h34t.filemure.TemplateModifiers import net.h34t.filemure.*
import net.h34t.filemure.formatHumanShort
import net.h34t.filemure.repository.SqliteRepository import net.h34t.filemure.repository.SqliteRepository
import net.h34t.filemure.requireSession
import net.h34t.filemure.tempolin
import net.h34t.filemure.tpl.Frame import net.h34t.filemure.tpl.Frame
import net.h34t.filemure.tpl.Search import net.h34t.filemure.tpl.Search
import net.h34t.filemure.tpl.Tags
class SearchController(val modifiers: TemplateModifiers, val repository: SqliteRepository) { class SearchController(val modifiers: TemplateModifiers, val repository: SqliteRepository) {
@ -42,4 +40,36 @@ class SearchController(val modifiers: TemplateModifiers, val repository: SqliteR
) )
} }
fun tags(ctx: Context) {
val session = ctx.requireSession()
val t = ctx.queryParams("t").map { Tag.of(it) }
val tags = repository.getAllTags(session.id)
val documents = repository.getDocumentsByTags(accountId = session.id, tags = t)
ctx.tempolin(
Frame(
modifiers = modifiers,
title = "Search",
target = "document",
back = "/",
logout = true,
content = Tags(
modifiers = modifiers,
tag = { tags.map { TagBlock(tag = it.value) }.asSequence() },
document = {
documents.map { d ->
DocumentBlock(
extId = d.extId.value,
title = d.title,
referenceDate = d.referenceDate.formatHumanShort()
)
}.asSequence()
}
)
)
)
}
} }

View file

@ -317,7 +317,7 @@ class SqliteRepository(url: String) {
} }
fun searchDocuments(accountId: Long, state: State = State.ACTIVE, query: String): List<Document> { fun searchDocuments(accountId: Long, state: State = State.ACTIVE, query: String): List<Document> {
return database.databaseQueries.searchDocument(account_id = accountId, state = state, query = query) return database.databaseQueries.searchDocuments(account_id = accountId, state = state, query = query)
.executeAsList() .executeAsList()
.map { .map {
Document( Document(
@ -359,5 +359,28 @@ class SqliteRepository(url: String) {
database.databaseQueries.setFileState(account_id = accountId, ext_id = extId, state = state) database.databaseQueries.setFileState(account_id = accountId, ext_id = extId, state = state)
} }
fun getAllTags(accountId: Long): List<Tag> =
database.databaseQueries.getAllTags(accountId).executeAsList().flatten().distinct()
private fun lastInsertedId() = database.databaseQueries.getLastInsertRowId().executeAsOne() private fun lastInsertedId() = database.databaseQueries.getLastInsertRowId().executeAsOne()
fun getDocumentsByTags(accountId: Long, state: State = State.ACTIVE, tags: List<Tag>): List<Document> {
return database.databaseQueries.getDocuments(account_id = accountId, state = state)
.executeAsList()
.filter { d -> (d.tags intersect tags).isNotEmpty() }
.map {
Document(
id = it.id,
extId = it.ext_id,
title = it.title,
description = it.description,
tags = it.tags,
created = it.created,
referenceDate = it.reference_date,
state = it.state,
files = emptyList()
)
}
}
} }

View file

@ -219,7 +219,7 @@ UPDATE file SET state=? WHERE account_id=? AND ext_id=?;
setFilesState: setFilesState:
UPDATE file SET state=? WHERE account_id=? AND ext_id IN ?; UPDATE file SET state=? WHERE account_id=? AND ext_id IN ?;
searchDocument: searchDocuments:
SELECT SELECT
d.id, d.id,
d.account_id, d.account_id,
@ -241,3 +241,5 @@ WHERE
f.filename LIKE :query OR f.filename LIKE :query OR
f.content_extracted LIKE :query); f.content_extracted LIKE :query);
getAllTags:
SELECT tags FROM document WHERE account_id=:accountId;

View file

@ -0,0 +1,39 @@
<form action="/tags" method="get">
<fieldset>
<legend>Tags</legend>
<div class="tags">
{for $tag}
<button class="chip fill round tag">
<i>done</i>
<span>{*$tag}</span>
</button>
{/for}
</div>
</fieldset>
</form>
<fieldset>
<legend>Results</legend>
<table class="stripes">
<thead>
<tr>
<th>Date</th>
<th>Title</th>
<th>Details</th>
</tr>
</thead>
<tbody>
{for $document}
<tr>
<td>{*$referenceDate}</td>
<td>{*$title}</td>
<td><a href="/document/{*$extId}">
<button>details</button>
</a></td>
</tr>
{/for}
</tbody>
</table>
</fieldset>