implemented sharing of bookmarks
This commit is contained in:
@@ -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<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){
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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<Long> userIds);
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<Long> 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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
</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>
|
||||
{#each Object.entries(options) as [val,caption]}
|
||||
<option value={val}>{caption}</option>
|
||||
{/each}
|
||||
</select>
|
||||
|
||||
{/if}
|
||||
|
||||
@@ -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);
|
||||
|
||||
50
frontend/src/Components/UserSelector.svelte
Normal file
50
frontend/src/Components/UserSelector.svelte
Normal file
@@ -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>
|
||||
@@ -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,12 +85,16 @@
|
||||
{/if}
|
||||
<label>
|
||||
{t('URL')}
|
||||
<input bind:value={new_bookmark.url} />
|
||||
<input bind:value={new_bookmark.url} autofocus />
|
||||
</label>
|
||||
<label>
|
||||
{t('Comment')}
|
||||
<Editor simple={true} bind:value={new_bookmark.comment} />
|
||||
</label>
|
||||
<label>
|
||||
{t('share_with')}
|
||||
<Users {getCandidates} users={new_bookmark.users} />
|
||||
</label>
|
||||
<Tags module="bookmark" bind:tags={new_bookmark.tags} />
|
||||
<button {onclick}>{t('save')}</button>
|
||||
{#if bookmarks}
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
|
||||
<div class="taglist">
|
||||
<span class="tag editor">
|
||||
<input type="text" bind:value={newTag} onkeyup={typed} autofocus />
|
||||
<input type="text" bind:value={newTag} onkeyup={typed} />
|
||||
</span>
|
||||
{#each tags as tag,idx}
|
||||
<span class="tag">
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"add_state": "Status hinzufügen",
|
||||
"add_subtask": "Unteraufgabe hinzufügen",
|
||||
"add_task": "Aufgabe hinzufügen",
|
||||
"add_user": "Benutzer hinzufügen",
|
||||
"advertisement" : "Umbrella ist ein Produkt von {producer}.",
|
||||
"allowed_states": "zulässige Status",
|
||||
"amount": "Menge",
|
||||
@@ -173,6 +174,7 @@
|
||||
"repeat_new_password": "Wiederholung",
|
||||
"results": "Ergebnisse",
|
||||
|
||||
"save": "speichern",
|
||||
"saved": "gespeichert",
|
||||
"save_note": "Notiz speichern",
|
||||
"save_service": "Service speichern",
|
||||
@@ -191,6 +193,7 @@
|
||||
"sent_email": "Email gesendet",
|
||||
"service": "Service",
|
||||
"settings" : "Einstellungen",
|
||||
"share_with": "Teilen mit:",
|
||||
"show": "anzeigen",
|
||||
"show_closed": "geschlossene anzeigen",
|
||||
"show_kanban": "Kanban-Ansicht",
|
||||
|
||||
Reference in New Issue
Block a user