Compare commits

..
3 Commits
Author SHA1 Message Date
StephanRichter d699bd8b7e finally implemented tag search
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-11-09 22:37:06 +01:00
StephanRichter 7411bc1583 working on tag search
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-11-09 18:53:24 +01:00
StephanRichter 6fa9f419d4 workin on search based on tags
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-11-09 17:04:44 +01:00
2 changed files with 171 additions and 32 deletions
+150 -28
View File
@@ -8,25 +8,38 @@
import Bookmark from '../bookmark/Template.svelte';
let fulltext = false;
let counter = 0;
const router = useTinyRouter();
let counter = 9;
let key = $derived(router.getQueryParam('key'));
search.key = key;
let bookmarks = $state(null);
let companies = $state(null);
let documents = $state(null);
let fulltext = false;
let notes = $state(null);
let pages = $state(null);
let projects = $state(null);
let stock = $state(null);
let tags = $state(null);
let tasks = $state(null);
let times = $state(null);
function doSearch(key){
function doSearch(){
if (counter > 0) return;
warn(t('searching…'));
counter = 10;
bookmarks = null;
companies = null;
documents = null;
notes = null;
pages = null;
projects = null;
stock = null;
tags = null;
tasks = null;
times = null;
const data = { key : key, fulltext : fulltext };
let data = { key, fulltext : fulltext };
post(api('bookmark/search'),data).then(handleBookmarks);
post(api('company/search '),data).then(handleCompanies);
post(api('document/search'),data).then(handleDocuments);
@@ -110,6 +123,87 @@
}
}
async function handleTags(resp){
if (resp.ok){
const json = await resp.json();
let map = {};
for (let [module, ids] of Object.entries(json)){
let list = {};
let path = module;
map[module] = {};
switch(module){
case 'bookmark': {
if (!bookmarks) bookmarks = {};
list = bookmarks;
break;
}
case 'company': {
if (!companies) companies = {};
list = companies;
break;
}
case 'document': {
if (!documents) documents = {};
list = documents;
break;
}
case 'note': {
if (!notes) notes = {};
list = notes;
break;
}
case 'wiki': {
if (!pages) pages = {};
list = pages;
break;
}
case 'project': {
if (!projects) projects = {};
list = projects;
break;
}
case 'stock': {
if (!stock) stock = { items : {}};
list = stock.items;
path = 'stock/item';
break;
}
case 'task': {
if (!tasks) tasks = {};
list = tasks;
break;
}
case 'time': {
if (!times) times = {};
list = times;
break;
}
default:{
console.warn("no handler for "+module+" with tag "+key);
continue;
}
}
for (let id of ids){
if (!list[id]) {
const url = api(`${path}/${id}`);
const resp = await get(url);
if (resp.ok) {
list[id] = await resp.json();
} else {
console.warn(`Failed to fetch ${module} ${id}: ${resp.statusText}`);
}
}
map[module][id] = list[id];
}
}
tags = map;
} else {
error(resp);
}
quitOne();
}
async function handleTasks(resp){
quitOne();
if (resp.ok){
@@ -142,16 +236,18 @@
function onclick(e){
e.preventDefault();
var target = e.target;
while (target && !target.href) target=target.parentNode;
let target = e.target;
let href = target.getAttribute('href');
if (href) router.navigate(href);
target = target.getAttribute('target');
if (target == '_blank') {
window.open(href);
} else if (href) router.navigate(href);
return false;
}
async function setKey(ev){
if (ev) ev.preventDefault();
doSearch(search.key);
doSearch();
}
async function setTitle(resp,key,module){
@@ -169,26 +265,28 @@
counter--;
if (counter > 0) {
warn(t('searching…')+" "+counter);
if (counter == 1) {
let data = { key, fulltext : fulltext };
post(api('tags/search'),data).then(handleTags);
}
} else yikes();
}
function updateUrl(ev){
ev.preventDefault();
let url = window.location.origin + window.location.pathname;
if (search.key) {
url += '?key=' + encodeURI(search.key);
window.history.replaceState(history.state, '', url);
doSearch(search.key);
}
if (search.key) router.navigate(window.location.pathname + '?key=' + encodeURI(search.key));
}
$effect(() => {
doSearch(router.getQueryParam('key'));
key = router.getQueryParam('key');
doSearch();
});
</script>
<svelte:head>
<title>Umbrella – {t('search')}{search.key?': '+search.key:''}</title>
<title>Umbrella – {t('search')}{key?': '+key:''}</title>
</svelte:head>
<fieldset class="search">
@@ -205,6 +303,23 @@
<button type="submit">{t('go')}</button>
</form>
</fieldset>
{#if tags && Object.keys(tags).length}
<fieldset>
<legend>
{t('tagged with "{tag}"',{tag:key})}
</legend>
{#each Object.entries(tags) as [module,list]}
<h4>{t(module)}</h4>
<ul>
{#each Object.entries(list) as [id,item]}
<li>
<a target="_blank" href="/{module}/{id}/view" {onclick} >{item?.name?item.name:(item?.comment?.source?item.comment.source:(item?.type?.name?t(item.type.name)+" "+item?.number:t(module)+" "+id))}</a>
</li>
{/each}
</ul>
{/each}
</fieldset>
{/if}
{#if projects}
<fieldset>
<legend>
@@ -213,7 +328,7 @@
<ul>
{#each Object.values(projects) as project}
<li>
<a href="/project/{project.id}/view" {onclick} >{project.name}</a>
<a target="_blank" href="/project/{project.id}/view" {onclick} >{project.name}</a>
</li>
{/each}
</ul>
@@ -227,7 +342,7 @@
<ul>
{#each Object.values(tasks) as task}
<li>
<a href="/task/{task.id}/view" {onclick} class={task.status.name}>{task.name}</a>
<a target="_blank" href="/task/{task.id}/view" {onclick} class={task.status.name}>{task.name}</a>
</li>
{/each}
</ul>
@@ -255,13 +370,13 @@
<ul>
{#each Object.values(companies) as company}
<li>
<a href="/company/{company.id}/view" {onclick} >{company.name}</a>
<a target="_blank" href="/company/{company.id}/view" {onclick} >{company.name}</a>
</li>
{/each}
</ul>
</fieldset>
{/if}
{#if stock}
{#if stock && (Object.keys(stock.items).length || Object.keys(stock.locations).length)}
<fieldset>
<legend>
{t('stock')}
@@ -271,7 +386,7 @@
<ul>
{#each Object.values(stock.items) as item}
<li>
<a href="/stock/{item.owner.type}/{item.owner.id}/item/{item.owner_number}" {onclick} >
<a target="_blank" href="/stock/{item.owner.type}/{item.owner.id}/item/{item.owner_number}" {onclick} >
{item.name} [{t('code')}: <span class="code">{item.code}</span>]
</a>
{@html item.description.rendered}
@@ -284,7 +399,7 @@
<ul>
{#each Object.values(stock.locations) as loc (loc.id)}
<li>
<a href="/stock/location/{loc.id}">{loc.name}</a>
<a target="_blank" href="/stock/location/{loc.id}">{loc.name}</a>
<br/> {@html loc.description.rendered}
</li>
{/each}
@@ -300,7 +415,7 @@
<ul>
{#each Object.values(times) as time}
<li>
<a href="/time/{time.id}/view" {onclick} >
<a target="_blank" href="/time/{time.id}/view" {onclick} >
{display(time.start_time)}{#if time.end_time}…{display(time.end_time)}{/if}
{time.subject}
</a>
@@ -319,7 +434,7 @@
<ul>
{#each Object.values(documents) as document}
<li>
<a href="/document/{document.id}/view" {onclick} >
<a target="_blank" href="/document/{document.id}/view" {onclick} >
{document.number}
({document.date})
{document.sender.name.split('\n')[0]}
@@ -339,7 +454,7 @@
<ul>
{#each Object.values(pages) as page}
<li>
<a href="/wiki/{page.id}/view" {onclick} >
<a target="_blank" href="/wiki/{page.id}/view" {onclick} >
{page.title}
</a>
</li>
@@ -356,7 +471,7 @@
{#each Object.values(notes) as note}
<li>
<b>
<a href="/{note.module}/{note.entity_id}/view" {onclick} >{note.title}</a>
<a target="_blank" href="/{note.module}/{note.entity_id}/view" {onclick} >{note.title}</a>
</b>
{@html target(note.text.rendered)}
</li>
@@ -364,3 +479,10 @@
</ul>
</fieldset>
{/if}
<!--
<pre>
<code>
{JSON.stringify(tags,null,2)}
</code>
</pre>-->
@@ -4,6 +4,7 @@ package de.srsoftware.umbrella.tags;
import static de.srsoftware.umbrella.core.ConnectionProvider.connect;
import static de.srsoftware.umbrella.core.ModuleRegistry.userService;
import static de.srsoftware.umbrella.core.ResponseCode.HTTP_UNPROCESSABLE;
import static de.srsoftware.umbrella.core.constants.Field.KEY;
import static de.srsoftware.umbrella.core.constants.Field.USER_LIST;
import static de.srsoftware.umbrella.core.constants.Path.SEARCH;
import static de.srsoftware.umbrella.core.constants.Path.USES;
@@ -94,7 +95,18 @@ public class TagModule extends BaseHandler implements TagService {
var user = userService().loadUser(token);
if (user.isEmpty()) return unauthorized(ex);
var module = path.pop();
if (module == null) throw unprocessable("Module missing in path.");
return switch (module) {
case null -> throw unprocessable("Module missing in path.");
case SEARCH -> postSearch(ex, path, user.get());
default -> postTag(ex, module, path);
};
} catch (UmbrellaException e) {
return send(ex, e);
}
}
private boolean postTag(HttpExchange ex, String module, Path path) throws IOException {
try {
var head = path.pop();
long entityId = Long.parseLong(head);
var json = json(ex);
@@ -102,7 +114,7 @@ public class TagModule extends BaseHandler implements TagService {
List<Long> userList = null;
if (!json.has(USER_LIST)) throw missingField(USER_LIST);
var ul = json.isNull(USER_LIST) ? null : json.get(USER_LIST);
if (ul instanceof JSONArray arr){
if (ul instanceof JSONArray arr) {
userList = arr.toList().stream()
.filter(elem -> elem instanceof Number)
.map(Number.class::cast)
@@ -115,8 +127,6 @@ public class TagModule extends BaseHandler implements TagService {
return sendContent(ex, tag);
} catch (NumberFormatException e) {
return sendContent(ex, HTTP_UNPROCESSABLE, "Entity id missing in path.");
} catch (UmbrellaException e) {
return send(ex, e);
}
}
@@ -149,6 +159,13 @@ public class TagModule extends BaseHandler implements TagService {
return entry;
}
private boolean postSearch(HttpExchange ex, Path path, UmbrellaUser user) throws IOException {
var json = json(ex);
if (!json.has(KEY)) throw missingField(KEY);
var key = json.getString(KEY);
return getTagUses(ex,key,user);
}
@Override
public void save(String module, long entityId, Collection<Long> userIds, Collection<String> tags) {
tagDb.save(userIds,module,entityId,tags.stream().map(String::trim).toList());