From d7fe16c46e8edeae3e544389498a622bb0621235 Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Sun, 3 Aug 2025 22:42:37 +0200 Subject: [PATCH] implemented sharing of bookmarks --- .../umbrella/bookmarks/BookmarkApi.java | 19 +++++-- .../umbrella/bookmarks/BookmarkDb.java | 3 +- .../umbrella/bookmarks/Constants.java | 1 + .../umbrella/bookmarks/SqliteDb.java | 9 ++-- frontend/src/Components/Autocomplete.svelte | 8 +-- frontend/src/Components/MemberEditor.svelte | 8 +-- frontend/src/Components/UserSelector.svelte | 50 +++++++++++++++++++ frontend/src/routes/bookmark/Index.svelte | 41 +++++++++++---- frontend/src/routes/tags/TagList.svelte | 4 +- translations/src/main/resources/de.json | 3 ++ 10 files changed, 118 insertions(+), 28 deletions(-) create mode 100644 frontend/src/Components/UserSelector.svelte diff --git a/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/BookmarkApi.java b/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/BookmarkApi.java index dbe54d6..e3e97d2 100644 --- a/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/BookmarkApi.java +++ b/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/BookmarkApi.java @@ -20,7 +20,7 @@ import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import de.srsoftware.umbrella.core.model.Token; import de.srsoftware.umbrella.core.model.UmbrellaUser; import java.io.IOException; -import java.util.List; +import java.util.ArrayList; import java.util.Optional; import org.json.JSONArray; @@ -74,8 +74,8 @@ public class BookmarkApi extends BaseHandler { if (user.isEmpty()) return unauthorized(ex); var head = path.pop(); return switch (head) { - case SAVE -> postBookmark(user.get(),ex); - case null, default -> super.doPost(path,ex); + case null -> postBookmark(user.get(),ex); + default -> super.doPost(path,ex); }; } catch (NumberFormatException e){ return sendContent(ex,HTTP_BAD_REQUEST,"Invalid project id"); @@ -93,10 +93,19 @@ public class BookmarkApi extends BaseHandler { var json = json(ex); if (!(json.has(URL) && json.get(URL) instanceof String url)) throw missingFieldException(URL); if (!(json.has(COMMENT) && json.get(COMMENT) instanceof String comment)) throw missingFieldException(COMMENT); - var bookmark = db.save(url,comment, user.id()); + var userList = new ArrayList(); + userList.add(user.id()); + if (json.has(SHARE) && json.get(SHARE) instanceof JSONArray arr){ + for (Object o : arr.toList()) { + if (!(o instanceof Number uid)) throw UmbrellaException.invalidFieldException(SHARE,"Array of ids"); + userList.add(uid.longValue()); + } + } + var bookmark = db.save(url,comment, userList); + if (json.has(TAGS) && json.get(TAGS) instanceof JSONArray tagList){ var list = tagList.toList().stream().map(Object::toString).toList(); - tags.save(BOOKMARK,bookmark.id(), List.of(user.id()),list); + tags.save(BOOKMARK,bookmark.id(), userList, list); } return sendContent(ex,bookmark); } diff --git a/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/BookmarkDb.java b/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/BookmarkDb.java index e64f3b2..9c9d930 100644 --- a/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/BookmarkDb.java +++ b/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/BookmarkDb.java @@ -2,6 +2,7 @@ package de.srsoftware.umbrella.bookmarks; import de.srsoftware.umbrella.core.model.Bookmark; +import java.util.Collection; import java.util.Map; public interface BookmarkDb { @@ -9,5 +10,5 @@ public interface BookmarkDb { Bookmark load(long id, long userId); - Bookmark save(String url, String comment, long userId); + Bookmark save(String url, String comment, Collection userIds); } diff --git a/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/Constants.java b/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/Constants.java index e80e195..0ed5343 100644 --- a/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/Constants.java +++ b/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/Constants.java @@ -3,6 +3,7 @@ package de.srsoftware.umbrella.bookmarks; public class Constants { public static final String CONFIG_DATABASE = "umbrella.modules.bookmark.database"; + public static final String SHARE = "share"; public static final String SAVE = "save"; public static final String TABLE_URLS = "urls"; public static final String TABLE_URL_COMMENTS = "url_comments"; diff --git a/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/SqliteDb.java b/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/SqliteDb.java index a6bb26d..680db69 100644 --- a/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/SqliteDb.java +++ b/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/SqliteDb.java @@ -17,6 +17,7 @@ import de.srsoftware.umbrella.core.model.Bookmark; import java.sql.Connection; import java.sql.SQLException; import java.time.LocalDateTime; +import java.util.Collection; import java.util.HashMap; import java.util.Map; @@ -98,7 +99,7 @@ CREATE TABLE IF NOT EXISTS {0} ( } @Override - public Bookmark save(String url, String comment, long userId) { + public Bookmark save(String url, String comment, Collection userIds) { try { var timestamp = LocalDateTime.now(); var rs = select(ID).from(TABLE_URLS).where(URL, equal(url)).exec(db); @@ -112,9 +113,9 @@ CREATE TABLE IF NOT EXISTS {0} ( rs.close(); stmt.close(); } - replaceInto(TABLE_URL_COMMENTS,URL_ID,USER_ID,COMMENT, TIMESTAMP) - .values(id,userId,comment,timestamp.toEpochSecond(UTC)) - .execute(db).close(); + var query = replaceInto(TABLE_URL_COMMENTS,URL_ID,USER_ID,COMMENT, TIMESTAMP); + for (long userId : userIds) query.values(id,userId,comment,timestamp.toEpochSecond(UTC)); + query.execute(db).close(); return Bookmark.of(id,url,comment,timestamp); } catch (SQLException e) { throw new UmbrellaException("Failed to store url"); diff --git a/frontend/src/Components/Autocomplete.svelte b/frontend/src/Components/Autocomplete.svelte index 2985edf..08d263f 100644 --- a/frontend/src/Components/Autocomplete.svelte +++ b/frontend/src/Components/Autocomplete.svelte @@ -3,7 +3,7 @@ import { tick } from "svelte"; let { - getCandidates = text => {}, + getCandidates = async text => { conole.log('no handler for getCandidates('+text+')'); return {};}, onSelect = text => [] } = $props(); @@ -18,6 +18,7 @@ let result = {}; result[key] = text; options = {}; + text = ''; onSelect(result); } @@ -49,10 +50,11 @@ min-width: 200px; } - {#each Object.entries(options) as [val,caption]} {/each} - +{/if} diff --git a/frontend/src/Components/MemberEditor.svelte b/frontend/src/Components/MemberEditor.svelte index 7b9df9c..b37ac71 100644 --- a/frontend/src/Components/MemberEditor.svelte +++ b/frontend/src/Components/MemberEditor.svelte @@ -8,11 +8,11 @@ import PermissionSelector from './PermissionSelector.svelte'; let { - members, - getCandidates = text => {}, - updatePermission = (uid,perm) => console.log(`no handler for updatePermission(${uid}, ${perm})`), + addMember = (entry) => console.log(`no handler for addMember(${entry})`), dropMember = (member) => console.log(`no handler for dropMember(${member})`), - addMember = (entry) => console.log(`no handler for addMember(${entry})`) + getCandidates = text => {}, + members, + updatePermission = (uid,perm) => console.log(`no handler for updatePermission(${uid}, ${perm})`) } = $props(); let error = $state(null); diff --git a/frontend/src/Components/UserSelector.svelte b/frontend/src/Components/UserSelector.svelte new file mode 100644 index 0000000..af9c733 --- /dev/null +++ b/frontend/src/Components/UserSelector.svelte @@ -0,0 +1,50 @@ + + +{#if error} +{error} +{/if} + + + {#each sortedUsers as usr,idx} + + + + + {/each} + + + + + +
{usr.name} + {#if usr.id != user.id} + + {/if} +
{heading} + +
diff --git a/frontend/src/routes/bookmark/Index.svelte b/frontend/src/routes/bookmark/Index.svelte index aa5a79b..201a70c 100644 --- a/frontend/src/routes/bookmark/Index.svelte +++ b/frontend/src/routes/bookmark/Index.svelte @@ -5,6 +5,7 @@ import { t } from '../../translations.svelte.js'; import Editor from '../../Components/MarkdownEditor.svelte'; + import Users from '../../Components/UserSelector.svelte'; import Tags from '../tags/TagList.svelte'; import Template from './Template.svelte'; @@ -15,20 +16,36 @@ rendered:null }, tags:[], - url:null + url:null, + users:{} }); let error = $state(null); + async function getCandidates(text){ + const url = api('user/search'); + const resp = await fetch(url,{ + credentials : 'include', + method : 'POST', + body : text + }); + if (resp.ok){ + error = null; + const input = await resp.json(); + return Object.fromEntries( + Object.entries(input).map(([key, value]) => [key, value.name]) + ); + } else { + error = await resp.text(); + return {}; + } + } + async function loadBookmarks(){ const url = api('bookmark/list'); const resp = await fetch(url,{credentials:'include'}); if (resp.ok){ const raw = await resp.json(); - bookmarks = Object.values(raw) - .sort( - (a, b) => new Date(b.timestamp) - new Date(a.timestamp) - ); - + bookmarks = Object.values(raw).sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)); error = false; } else { error = await resp.html(); @@ -37,12 +54,14 @@ async function onclick(ev){ delete new_bookmark.comment.rendered; - const url = api('bookmark/save'); + const url = api('bookmark'); const data = { comment : new_bookmark.comment.source, url : new_bookmark.url, tags : new_bookmark.tags } + const share = Object.keys(new_bookmark.users).map(id => +id); + if (share.length>0) data.share = share; const resp = await fetch(url,{ credentials : 'include', method : 'POST', @@ -66,17 +85,21 @@ {/if} + {#if bookmarks} {#each bookmarks as bookmark} -