Browse Source

implemented sharing of bookmarks

featue/module_registry
Stephan Richter 3 months ago
parent
commit
d7fe16c46e
  1. 19
      bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/BookmarkApi.java
  2. 3
      bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/BookmarkDb.java
  3. 1
      bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/Constants.java
  4. 9
      bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/SqliteDb.java
  5. 8
      frontend/src/Components/Autocomplete.svelte
  6. 8
      frontend/src/Components/MemberEditor.svelte
  7. 50
      frontend/src/Components/UserSelector.svelte
  8. 41
      frontend/src/routes/bookmark/Index.svelte
  9. 4
      frontend/src/routes/tags/TagList.svelte
  10. 3
      translations/src/main/resources/de.json

19
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.Token;
import de.srsoftware.umbrella.core.model.UmbrellaUser; import de.srsoftware.umbrella.core.model.UmbrellaUser;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.ArrayList;
import java.util.Optional; import java.util.Optional;
import org.json.JSONArray; import org.json.JSONArray;
@ -74,8 +74,8 @@ public class BookmarkApi extends BaseHandler {
if (user.isEmpty()) return unauthorized(ex); if (user.isEmpty()) return unauthorized(ex);
var head = path.pop(); var head = path.pop();
return switch (head) { return switch (head) {
case SAVE -> postBookmark(user.get(),ex); case null -> postBookmark(user.get(),ex);
case null, default -> super.doPost(path,ex); default -> super.doPost(path,ex);
}; };
} catch (NumberFormatException e){ } catch (NumberFormatException e){
return sendContent(ex,HTTP_BAD_REQUEST,"Invalid project id"); return sendContent(ex,HTTP_BAD_REQUEST,"Invalid project id");
@ -93,10 +93,19 @@ public class BookmarkApi extends BaseHandler {
var json = json(ex); var json = json(ex);
if (!(json.has(URL) && json.get(URL) instanceof String url)) throw missingFieldException(URL); 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); 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<Long>();
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){ if (json.has(TAGS) && json.get(TAGS) instanceof JSONArray tagList){
var list = tagList.toList().stream().map(Object::toString).toList(); 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); return sendContent(ex,bookmark);
} }

3
bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/BookmarkDb.java

@ -2,6 +2,7 @@
package de.srsoftware.umbrella.bookmarks; package de.srsoftware.umbrella.bookmarks;
import de.srsoftware.umbrella.core.model.Bookmark; import de.srsoftware.umbrella.core.model.Bookmark;
import java.util.Collection;
import java.util.Map; import java.util.Map;
public interface BookmarkDb { public interface BookmarkDb {
@ -9,5 +10,5 @@ public interface BookmarkDb {
Bookmark load(long id, long userId); Bookmark load(long id, long userId);
Bookmark save(String url, String comment, long userId); Bookmark save(String url, String comment, Collection<Long> userIds);
} }

1
bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/Constants.java

@ -3,6 +3,7 @@ package de.srsoftware.umbrella.bookmarks;
public class Constants { public class Constants {
public static final String CONFIG_DATABASE = "umbrella.modules.bookmark.database"; 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 SAVE = "save";
public static final String TABLE_URLS = "urls"; public static final String TABLE_URLS = "urls";
public static final String TABLE_URL_COMMENTS = "url_comments"; public static final String TABLE_URL_COMMENTS = "url_comments";

9
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.Connection;
import java.sql.SQLException; import java.sql.SQLException;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@ -98,7 +99,7 @@ CREATE TABLE IF NOT EXISTS {0} (
} }
@Override @Override
public Bookmark save(String url, String comment, long userId) { public Bookmark save(String url, String comment, Collection<Long> userIds) {
try { try {
var timestamp = LocalDateTime.now(); var timestamp = LocalDateTime.now();
var rs = select(ID).from(TABLE_URLS).where(URL, equal(url)).exec(db); 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(); rs.close();
stmt.close(); stmt.close();
} }
replaceInto(TABLE_URL_COMMENTS,URL_ID,USER_ID,COMMENT, TIMESTAMP) var query = replaceInto(TABLE_URL_COMMENTS,URL_ID,USER_ID,COMMENT, TIMESTAMP);
.values(id,userId,comment,timestamp.toEpochSecond(UTC)) for (long userId : userIds) query.values(id,userId,comment,timestamp.toEpochSecond(UTC));
.execute(db).close(); query.execute(db).close();
return Bookmark.of(id,url,comment,timestamp); return Bookmark.of(id,url,comment,timestamp);
} catch (SQLException e) { } catch (SQLException e) {
throw new UmbrellaException("Failed to store url"); throw new UmbrellaException("Failed to store url");

8
frontend/src/Components/Autocomplete.svelte

@ -3,7 +3,7 @@
import { tick } from "svelte"; import { tick } from "svelte";
let { let {
getCandidates = text => {}, getCandidates = async text => { conole.log('no handler for getCandidates('+text+')'); return {};},
onSelect = text => [] onSelect = text => []
} = $props(); } = $props();
@ -18,6 +18,7 @@
let result = {}; let result = {};
result[key] = text; result[key] = text;
options = {}; options = {};
text = '';
onSelect(result); onSelect(result);
} }
@ -49,10 +50,11 @@
min-width: 200px; min-width: 200px;
} }
</style> </style>
<select size={Object.keys(options).length<2?2:Object.keys(options).length+1} {onkeyup} {ondblclick} autofocus width="40"> {#if options}
<select size={Object.keys(options).length<2?2:Object.keys(options).length+1} {onkeyup} {ondblclick} width="40">
<option>{text}</option> <option>{text}</option>
{#each Object.entries(options) as [val,caption]} {#each Object.entries(options) as [val,caption]}
<option value={val}>{caption}</option> <option value={val}>{caption}</option>
{/each} {/each}
</select> </select>
{/if}

8
frontend/src/Components/MemberEditor.svelte

@ -8,11 +8,11 @@
import PermissionSelector from './PermissionSelector.svelte'; import PermissionSelector from './PermissionSelector.svelte';
let { let {
members, addMember = (entry) => console.log(`no handler for addMember(${entry})`),
getCandidates = text => {},
updatePermission = (uid,perm) => console.log(`no handler for updatePermission(${uid}, ${perm})`),
dropMember = (member) => console.log(`no handler for dropMember(${member})`), 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(); } = $props();
let error = $state(null); let error = $state(null);

50
frontend/src/Components/UserSelector.svelte

@ -0,0 +1,50 @@
<script>
import { api } from '../urls.svelte.js';
import { t } from '../translations.svelte.js';
import { user } from '../user.svelte.js';
import Autocomplete from './Autocomplete.svelte';
let error = $state(null);
let {
getCandidates = async text => {},
heading = t('add_user'),
users = $bindable({})
} = $props();
function dropUser(id){
delete users[id];
}
function onSelect(entry){
for (let [k,v] of Object.entries(entry)){
users[k] = {name:v,id:k};
}
}
let sortedUsers = $derived.by(() => Object.values(users).sort((a, b) => a.name.localeCompare(b.name)));
</script>
{#if error}
<span class="error">{error}</span>
{/if}
<table>
<tbody>
{#each sortedUsers as usr,idx}
<tr>
<td>{usr.name}</td>
<td>
{#if usr.id != user.id}
<button onclick={() => dropUser(usr.id)}>x</button>
{/if}
</td>
</tr>
{/each}
<tr>
<td>{heading}</td>
<td>
<Autocomplete {getCandidates} {onSelect} />
</td>
</tr>
</tbody>
</table>

41
frontend/src/routes/bookmark/Index.svelte

@ -5,6 +5,7 @@
import { t } from '../../translations.svelte.js'; import { t } from '../../translations.svelte.js';
import Editor from '../../Components/MarkdownEditor.svelte'; import Editor from '../../Components/MarkdownEditor.svelte';
import Users from '../../Components/UserSelector.svelte';
import Tags from '../tags/TagList.svelte'; import Tags from '../tags/TagList.svelte';
import Template from './Template.svelte'; import Template from './Template.svelte';
@ -15,20 +16,36 @@
rendered:null rendered:null
}, },
tags:[], tags:[],
url:null url:null,
users:{}
}); });
let error = $state(null); 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(){ async function loadBookmarks(){
const url = api('bookmark/list'); const url = api('bookmark/list');
const resp = await fetch(url,{credentials:'include'}); const resp = await fetch(url,{credentials:'include'});
if (resp.ok){ if (resp.ok){
const raw = await resp.json(); const raw = await resp.json();
bookmarks = Object.values(raw) bookmarks = Object.values(raw).sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
.sort(
(a, b) => new Date(b.timestamp) - new Date(a.timestamp)
);
error = false; error = false;
} else { } else {
error = await resp.html(); error = await resp.html();
@ -37,12 +54,14 @@
async function onclick(ev){ async function onclick(ev){
delete new_bookmark.comment.rendered; delete new_bookmark.comment.rendered;
const url = api('bookmark/save'); const url = api('bookmark');
const data = { const data = {
comment : new_bookmark.comment.source, comment : new_bookmark.comment.source,
url : new_bookmark.url, url : new_bookmark.url,
tags : new_bookmark.tags 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,{ const resp = await fetch(url,{
credentials : 'include', credentials : 'include',
method : 'POST', method : 'POST',
@ -66,17 +85,21 @@
{/if} {/if}
<label> <label>
{t('URL')} {t('URL')}
<input bind:value={new_bookmark.url} /> <input bind:value={new_bookmark.url} autofocus />
</label> </label>
<label> <label>
{t('Comment')} {t('Comment')}
<Editor simple={true} bind:value={new_bookmark.comment} /> <Editor simple={true} bind:value={new_bookmark.comment} />
</label> </label>
<label>
{t('share_with')}
<Users {getCandidates} users={new_bookmark.users} />
</label>
<Tags module="bookmark" bind:tags={new_bookmark.tags} /> <Tags module="bookmark" bind:tags={new_bookmark.tags} />
<button {onclick}>{t('save')}</button> <button {onclick}>{t('save')}</button>
{#if bookmarks} {#if bookmarks}
{#each bookmarks as bookmark} {#each bookmarks as bookmark}
<Template {bookmark} /> <Template {bookmark} />
{/each} {/each}
{/if} {/if}
</fieldset> </fieldset>

4
frontend/src/routes/tags/TagList.svelte

@ -101,7 +101,7 @@
<div class="taglist"> <div class="taglist">
<span class="tag editor"> <span class="tag editor">
<input type="text" bind:value={newTag} onkeyup={typed} autofocus /> <input type="text" bind:value={newTag} onkeyup={typed} />
</span> </span>
{#each tags as tag,idx} {#each tags as tag,idx}
<span class="tag"> <span class="tag">
@ -109,4 +109,4 @@
<button onclick={() => drop(tag)} class="symbol"></button> <button onclick={() => drop(tag)} class="symbol"></button>
</span> </span>
{/each} {/each}
</div> </div>

3
translations/src/main/resources/de.json

@ -7,6 +7,7 @@
"add_state": "Status hinzufügen", "add_state": "Status hinzufügen",
"add_subtask": "Unteraufgabe hinzufügen", "add_subtask": "Unteraufgabe hinzufügen",
"add_task": "Aufgabe hinzufügen", "add_task": "Aufgabe hinzufügen",
"add_user": "Benutzer hinzufügen",
"advertisement" : "Umbrella ist ein Produkt von {producer}.", "advertisement" : "Umbrella ist ein Produkt von {producer}.",
"allowed_states": "zulässige Status", "allowed_states": "zulässige Status",
"amount": "Menge", "amount": "Menge",
@ -173,6 +174,7 @@
"repeat_new_password": "Wiederholung", "repeat_new_password": "Wiederholung",
"results": "Ergebnisse", "results": "Ergebnisse",
"save": "speichern",
"saved": "gespeichert", "saved": "gespeichert",
"save_note": "Notiz speichern", "save_note": "Notiz speichern",
"save_service": "Service speichern", "save_service": "Service speichern",
@ -191,6 +193,7 @@
"sent_email": "Email gesendet", "sent_email": "Email gesendet",
"service": "Service", "service": "Service",
"settings" : "Einstellungen", "settings" : "Einstellungen",
"share_with": "Teilen mit:",
"show": "anzeigen", "show": "anzeigen",
"show_closed": "geschlossene anzeigen", "show_closed": "geschlossene anzeigen",
"show_kanban": "Kanban-Ansicht", "show_kanban": "Kanban-Ansicht",

Loading…
Cancel
Save