Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 410065f712 | |||
| 9a84fa6bc6 | |||
| 2d6b017352 | |||
| b71fd4492c | |||
| eede073cff | |||
| e4a9463307 | |||
| b73fb7c716 |
@@ -59,6 +59,7 @@ tasks.jar {
|
|||||||
":markdown:jar",
|
":markdown:jar",
|
||||||
":messages:jar",
|
":messages:jar",
|
||||||
":notes:jar",
|
":notes:jar",
|
||||||
|
":poll:jar",
|
||||||
":project:jar",
|
":project:jar",
|
||||||
":stock:jar",
|
":stock:jar",
|
||||||
":tags:jar",
|
":tags:jar",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
description = "Umbrella : Bookmarks"
|
description = "Umbrella : Bookmarks"
|
||||||
|
|
||||||
dependencies{
|
dependencies{
|
||||||
|
implementation(project(":bus"))
|
||||||
implementation(project(":core"))
|
implementation(project(":core"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
/* © SRSoftware 2025 */
|
/* © SRSoftware 2025 */
|
||||||
package de.srsoftware.umbrella.bookmarks;
|
package de.srsoftware.umbrella.bookmarks;
|
||||||
|
|
||||||
|
import static de.srsoftware.umbrella.messagebus.events.Event.EventType;
|
||||||
import static de.srsoftware.umbrella.bookmarks.Constants.*;
|
import static de.srsoftware.umbrella.bookmarks.Constants.*;
|
||||||
import static de.srsoftware.umbrella.core.ConnectionProvider.connect;
|
import static de.srsoftware.umbrella.core.ConnectionProvider.connect;
|
||||||
|
import static de.srsoftware.umbrella.core.ModuleRegistry.tagService;
|
||||||
import static de.srsoftware.umbrella.core.Util.mapValues;
|
import static de.srsoftware.umbrella.core.Util.mapValues;
|
||||||
import static de.srsoftware.umbrella.core.constants.Field.*;
|
import static de.srsoftware.umbrella.core.constants.Field.*;
|
||||||
import static de.srsoftware.umbrella.core.constants.Field.TAGS;
|
import static de.srsoftware.umbrella.core.constants.Field.TAGS;
|
||||||
@@ -10,7 +12,11 @@ import static de.srsoftware.umbrella.core.constants.Module.BOOKMARK;
|
|||||||
import static de.srsoftware.umbrella.core.constants.Path.LIST;
|
import static de.srsoftware.umbrella.core.constants.Path.LIST;
|
||||||
import static de.srsoftware.umbrella.core.constants.Path.SEARCH;
|
import static de.srsoftware.umbrella.core.constants.Path.SEARCH;
|
||||||
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*;
|
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*;
|
||||||
|
import static de.srsoftware.umbrella.messagebus.MessageBus.messageBus;
|
||||||
|
import static de.srsoftware.umbrella.messagebus.events.Event.EventType.CREATE;
|
||||||
|
import static de.srsoftware.umbrella.messagebus.events.Event.EventType.UPDATE;
|
||||||
import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
|
import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
|
||||||
|
import static java.net.HttpURLConnection.HTTP_OK;
|
||||||
|
|
||||||
import com.sun.net.httpserver.HttpExchange;
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
import de.srsoftware.configuration.Configuration;
|
import de.srsoftware.configuration.Configuration;
|
||||||
@@ -19,14 +25,18 @@ import de.srsoftware.tools.SessionToken;
|
|||||||
import de.srsoftware.umbrella.core.BaseHandler;
|
import de.srsoftware.umbrella.core.BaseHandler;
|
||||||
import de.srsoftware.umbrella.core.ModuleRegistry;
|
import de.srsoftware.umbrella.core.ModuleRegistry;
|
||||||
import de.srsoftware.umbrella.core.api.BookmarkService;
|
import de.srsoftware.umbrella.core.api.BookmarkService;
|
||||||
|
import de.srsoftware.umbrella.core.constants.Text;
|
||||||
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
|
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.awt.print.Book;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Map;
|
import de.srsoftware.umbrella.messagebus.events.BookmarkEvent;
|
||||||
import java.util.Optional;
|
import de.srsoftware.umbrella.messagebus.events.Event;
|
||||||
|
import de.srsoftware.umbrella.messagebus.events.TaskEvent;
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
|
|
||||||
public class BookmarkApi extends BaseHandler implements BookmarkService {
|
public class BookmarkApi extends BaseHandler implements BookmarkService {
|
||||||
@@ -39,6 +49,30 @@ public class BookmarkApi extends BaseHandler implements BookmarkService {
|
|||||||
ModuleRegistry.add(this);
|
ModuleRegistry.add(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean deleteBookmark(UmbrellaUser user, HttpExchange ex, long urlId) throws IOException {
|
||||||
|
var bookmark = db.load(urlId,user.id());
|
||||||
|
db.remove(user, bookmark);
|
||||||
|
return sendEmptyResponse(HTTP_OK,ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean doDelete(Path path, HttpExchange ex) throws IOException {
|
||||||
|
addCors(ex);
|
||||||
|
try {
|
||||||
|
Optional<Token> token = SessionToken.from(ex).map(Token::of);
|
||||||
|
var user = ModuleRegistry.userService().loadUser(token);
|
||||||
|
if (user.isEmpty()) return unauthorized(ex);
|
||||||
|
var head = path.pop();
|
||||||
|
if (head == null) throw missingField(ID);
|
||||||
|
var id = Long.parseLong(head);
|
||||||
|
return deleteBookmark(user.get(),ex,id);
|
||||||
|
} catch (NumberFormatException e){
|
||||||
|
throw invalidField(ID, Text.NUMBER);
|
||||||
|
} catch (UmbrellaException e){
|
||||||
|
return send(ex,e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean doGet(Path path, HttpExchange ex) throws IOException {
|
public boolean doGet(Path path, HttpExchange ex) throws IOException {
|
||||||
addCors(ex);
|
addCors(ex);
|
||||||
@@ -51,21 +85,33 @@ public class BookmarkApi extends BaseHandler implements BookmarkService {
|
|||||||
case LIST -> getUserBookmarks(user.get(),ex);
|
case LIST -> getUserBookmarks(user.get(),ex);
|
||||||
case null -> super.doPost(path,ex);
|
case null -> super.doPost(path,ex);
|
||||||
default -> {
|
default -> {
|
||||||
var id = Long.parseLong(head);
|
var urlId = Long.parseLong(head);
|
||||||
yield getBookmark(user.get(),id,ex);
|
yield getBookmark(user.get(),urlId,ex);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} catch (NumberFormatException e){
|
} catch (NumberFormatException e){
|
||||||
return sendContent(ex,HTTP_BAD_REQUEST,"Invalid project id");
|
throw invalidField(ID, Text.NUMBER);
|
||||||
} catch (UmbrellaException e){
|
} catch (UmbrellaException e){
|
||||||
return send(ex,e);
|
return send(ex,e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean getBookmark(UmbrellaUser user, long id, HttpExchange ex) throws IOException {
|
@Override
|
||||||
var bookmark = db.load(id,user.id());
|
public boolean doPatch(Path path, HttpExchange ex) throws IOException {
|
||||||
ModuleRegistry.tagService().getTags(BOOKMARK, id, user).forEach(bookmark.tags()::add);
|
addCors(ex);
|
||||||
return sendContent(ex,bookmark);
|
try {
|
||||||
|
Optional<Token> token = SessionToken.from(ex).map(Token::of);
|
||||||
|
var user = ModuleRegistry.userService().loadUser(token);
|
||||||
|
if (user.isEmpty()) return unauthorized(ex);
|
||||||
|
var head = path.pop();
|
||||||
|
if (head == null) throw missingField(ID);
|
||||||
|
var id = Long.parseLong(head);
|
||||||
|
return patchBookmark(user.get(),ex,id);
|
||||||
|
} catch (NumberFormatException e){
|
||||||
|
throw invalidField(ID, Text.NUMBER);
|
||||||
|
} catch (UmbrellaException e){
|
||||||
|
return send(ex,e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -93,6 +139,12 @@ public class BookmarkApi extends BaseHandler implements BookmarkService {
|
|||||||
return db.findUrls(key);
|
return db.findUrls(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean getBookmark(UmbrellaUser user, long urlId, HttpExchange ex) throws IOException {
|
||||||
|
var bookmark = db.load(urlId,user.id());
|
||||||
|
tagService().getTags(BOOKMARK, urlId, user).forEach(bookmark.tags()::add);
|
||||||
|
return sendContent(ex,bookmark);
|
||||||
|
}
|
||||||
|
|
||||||
private boolean getUserBookmarks(UmbrellaUser user, HttpExchange ex) throws IOException {
|
private boolean getUserBookmarks(UmbrellaUser user, HttpExchange ex) throws IOException {
|
||||||
var param = queryParam(ex);
|
var param = queryParam(ex);
|
||||||
long offset = switch (param.get(OFFSET)){
|
long offset = switch (param.get(OFFSET)){
|
||||||
@@ -110,6 +162,23 @@ public class BookmarkApi extends BaseHandler implements BookmarkService {
|
|||||||
return sendContent(ex,mapValues(bookmarks));
|
return sendContent(ex,mapValues(bookmarks));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean patchBookmark(UmbrellaUser user, HttpExchange ex, long urlId) throws IOException {
|
||||||
|
var bookmark = db.load(urlId,user.id());
|
||||||
|
var tags = tagService().getTags(BOOKMARK,urlId,user);
|
||||||
|
var json = json(ex);
|
||||||
|
var comment = bookmark.comment();
|
||||||
|
var url = bookmark.url();
|
||||||
|
if (json.has(COMMENT) && json.get(COMMENT) instanceof String c) comment = c;
|
||||||
|
if (json.has(URL) && json.get(URL) instanceof String u) url = u;
|
||||||
|
var newBookmark = db.save(url,comment, List.of(user.id()),bookmark.timestamp());
|
||||||
|
if (newBookmark.urlId() != urlId) {
|
||||||
|
tagService().save(BOOKMARK,newBookmark.urlId(),List.of(user.id()),tags);
|
||||||
|
db.remove(user, bookmark);
|
||||||
|
messageBus().dispatch(new BookmarkEvent(user,newBookmark,CREATE));
|
||||||
|
} else messageBus().dispatch(new BookmarkEvent(user,newBookmark,UPDATE));
|
||||||
|
return sendContent(ex,newBookmark);
|
||||||
|
}
|
||||||
|
|
||||||
private boolean postBookmark(UmbrellaUser user, HttpExchange ex) throws IOException {
|
private boolean postBookmark(UmbrellaUser user, HttpExchange ex) throws IOException {
|
||||||
var json = json(ex);
|
var json = json(ex);
|
||||||
if (!(json.has(URL) && json.get(URL) instanceof String url)) throw missingField(URL);
|
if (!(json.has(URL) && json.get(URL) instanceof String url)) throw missingField(URL);
|
||||||
@@ -123,10 +192,11 @@ public class BookmarkApi extends BaseHandler implements BookmarkService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
var bookmark = db.save(url,comment, userList);
|
var bookmark = db.save(url,comment, userList);
|
||||||
|
messageBus().dispatch(new BookmarkEvent(user,bookmark,CREATE));
|
||||||
|
|
||||||
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();
|
||||||
ModuleRegistry.tagService().save(BOOKMARK,bookmark.urlId(), userList, list);
|
tagService().save(BOOKMARK,bookmark.urlId(), userList, list);
|
||||||
}
|
}
|
||||||
return sendContent(ex,bookmark);
|
return sendContent(ex,bookmark);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
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 de.srsoftware.umbrella.core.model.UmbrellaUser;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -15,6 +17,8 @@ public interface BookmarkDb {
|
|||||||
|
|
||||||
Bookmark load(long id, long userId);
|
Bookmark load(long id, long userId);
|
||||||
|
|
||||||
|
void remove(UmbrellaUser user, Bookmark bookmark);
|
||||||
|
|
||||||
Bookmark save(String url, String comment, Collection<Long> userIds, LocalDateTime datetime);
|
Bookmark save(String url, String comment, Collection<Long> userIds, LocalDateTime datetime);
|
||||||
|
|
||||||
default Bookmark save(String url, String comment, Collection<Long> userIds){
|
default Bookmark save(String url, String comment, Collection<Long> userIds){
|
||||||
|
|||||||
@@ -7,15 +7,24 @@ import static de.srsoftware.tools.jdbc.Query.*;
|
|||||||
import static de.srsoftware.tools.jdbc.Query.SelectQuery.ALL;
|
import static de.srsoftware.tools.jdbc.Query.SelectQuery.ALL;
|
||||||
import static de.srsoftware.umbrella.bookmarks.Constants.*;
|
import static de.srsoftware.umbrella.bookmarks.Constants.*;
|
||||||
import static de.srsoftware.umbrella.core.Errors.*;
|
import static de.srsoftware.umbrella.core.Errors.*;
|
||||||
|
import static de.srsoftware.umbrella.core.ModuleRegistry.tagService;
|
||||||
import static de.srsoftware.umbrella.core.constants.Field.*;
|
import static de.srsoftware.umbrella.core.constants.Field.*;
|
||||||
import static de.srsoftware.umbrella.core.constants.Text.BOOKMARK;
|
import static de.srsoftware.umbrella.core.constants.Text.BOOKMARK;
|
||||||
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*;
|
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*;
|
||||||
|
import static de.srsoftware.umbrella.messagebus.MessageBus.messageBus;
|
||||||
|
import static de.srsoftware.umbrella.messagebus.events.Event.EventType.CREATE;
|
||||||
|
import static de.srsoftware.umbrella.messagebus.events.Event.EventType.UPDATE;
|
||||||
import static java.text.MessageFormat.format;
|
import static java.text.MessageFormat.format;
|
||||||
import static java.time.ZoneOffset.UTC;
|
import static java.time.ZoneOffset.UTC;
|
||||||
|
|
||||||
import de.srsoftware.umbrella.core.BaseDb;
|
import de.srsoftware.umbrella.core.BaseDb;
|
||||||
|
import de.srsoftware.umbrella.core.constants.Module;
|
||||||
import de.srsoftware.umbrella.core.model.Bookmark;
|
import de.srsoftware.umbrella.core.model.Bookmark;
|
||||||
import de.srsoftware.umbrella.core.model.Translatable;
|
import de.srsoftware.umbrella.core.model.Translatable;
|
||||||
|
import de.srsoftware.umbrella.core.model.UmbrellaUser;
|
||||||
|
import de.srsoftware.umbrella.messagebus.events.BookmarkEvent;
|
||||||
|
import de.srsoftware.umbrella.messagebus.events.Event;
|
||||||
|
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
@@ -117,16 +126,29 @@ CREATE TABLE IF NOT EXISTS {0} (
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Bookmark load(long id, long userId) {
|
public Bookmark load(long urlId, long userId) {
|
||||||
try {
|
try {
|
||||||
Bookmark result = null;
|
Bookmark result = null;
|
||||||
var rs = select(ALL).from(TABLE_URLS).leftJoin(ID,TABLE_URL_COMMENTS,URL_ID).where(ID,equal(id)).where(USER_ID,equal(userId)).exec(db);
|
var rs = select(ALL).from(TABLE_URLS).leftJoin(ID,TABLE_URL_COMMENTS,URL_ID).where(ID,equal(urlId)).where(USER_ID,equal(userId)).exec(db);
|
||||||
if (rs.next()) result = Bookmark.of(rs);
|
if (rs.next()) result = Bookmark.of(rs);
|
||||||
rs.close();
|
rs.close();
|
||||||
if (result != null) return result;
|
if (result != null) return result;
|
||||||
throw failedToLoadObject(Translatable.t(BOOKMARK),id);
|
throw failedToLoadObject(Translatable.t(BOOKMARK),urlId);
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
throw failedToLoadObject(Translatable.t(BOOKMARK),id).causedBy(e);
|
throw failedToLoadObject(Translatable.t(BOOKMARK),urlId).causedBy(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void remove(UmbrellaUser user, Bookmark bookmark) {
|
||||||
|
try {
|
||||||
|
var urlId = bookmark.urlId();
|
||||||
|
var userId = user.id();
|
||||||
|
delete().from(TABLE_URL_COMMENTS).where(USER_ID, equal(userId)).where(URL_ID, equal(urlId)).execute(db);
|
||||||
|
messageBus().dispatch(new BookmarkEvent(user,bookmark, Event.EventType.DELETE));
|
||||||
|
tagService().deleteEntity(Module.BOOKMARK,urlId,userId);
|
||||||
|
} catch (SQLException e){
|
||||||
|
throw failedToDropObject(BOOKMARK);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package de.srsoftware.umbrella.messagebus.events;
|
||||||
|
|
||||||
|
import de.srsoftware.umbrella.core.constants.Field;
|
||||||
|
import de.srsoftware.umbrella.core.model.Bookmark;
|
||||||
|
import de.srsoftware.umbrella.core.model.Task;
|
||||||
|
import de.srsoftware.umbrella.core.model.Translatable;
|
||||||
|
import de.srsoftware.umbrella.core.model.UmbrellaUser;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static de.srsoftware.umbrella.core.constants.Module.BOOKMARK;
|
||||||
|
import static de.srsoftware.umbrella.core.model.Translatable.t;
|
||||||
|
|
||||||
|
public class BookmarkEvent extends Event<Bookmark> {
|
||||||
|
|
||||||
|
public BookmarkEvent(UmbrellaUser initiator, Bookmark bookmark, EventType type){
|
||||||
|
super(initiator,BOOKMARK,bookmark,type);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public Collection<UmbrellaUser> audience() {
|
||||||
|
return List.of(initiator());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Translatable describe() {
|
||||||
|
return switch (eventType()){
|
||||||
|
case CREATE -> t("New bookmark created");
|
||||||
|
case DELETE -> t("The bookmark '{url}' has been deleted", Field.URL, payload().url());
|
||||||
|
case UPDATE -> t("Bookmark updated");
|
||||||
|
default -> null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Translatable subject() {
|
||||||
|
return describe();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -8,7 +8,9 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public interface TagService {
|
public interface TagService {
|
||||||
void deleteEntity(String task, long taskId);
|
void deleteEntity(String module, long taskId);
|
||||||
|
|
||||||
|
void deleteEntity(String module, long urlId, long userId);
|
||||||
|
|
||||||
Map<String, List<Long>> getTagUses(UmbrellaUser user, String tag);
|
Map<String, List<Long>> getTagUses(UmbrellaUser user, String tag);
|
||||||
|
|
||||||
|
|||||||
@@ -228,20 +228,19 @@ public class Task implements Mappable {
|
|||||||
return tags;
|
return tags;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public Map<String,Object> toMap(boolean renderMarkdown){
|
||||||
public Map<String, Object> toMap() {
|
|
||||||
var map = new HashMap<String,Object>();
|
var map = new HashMap<String,Object>();
|
||||||
var memberMap = new HashMap<Long,Map<String,Object>>();
|
var memberMap = new HashMap<Long,Map<String,Object>>();
|
||||||
if (members != null) for (var entry : members.entrySet()){
|
if (members != null) for (var entry : members.entrySet()){
|
||||||
memberMap.put(entry.getKey(),entry.getValue().toMap());
|
memberMap.put(entry.getKey(),entry.getValue().toMap());
|
||||||
}
|
}
|
||||||
map.put(ID, id);
|
map.put(ID, id);
|
||||||
map.put(PROJECT_ID, projectId);
|
map.put(PROJECT_ID, projectId);
|
||||||
map.put(PARENT_TASK_ID, parentTaskId);
|
map.put(PARENT_TASK_ID, parentTaskId);
|
||||||
map.put(PRIORITY,priority);
|
map.put(PRIORITY,priority);
|
||||||
map.put(NAME, name);
|
map.put(NAME, name);
|
||||||
map.put(DESCRIPTION, mapMarkdown(description));
|
map.put(DESCRIPTION, renderMarkdown ? mapMarkdown(description) : Map.of(SOURCE,description));
|
||||||
map.put(STATUS, status);
|
map.put(STATUS, status);
|
||||||
map.put(EST_TIME, estimatedTime);
|
map.put(EST_TIME, estimatedTime);
|
||||||
map.put(START_DATE,start);
|
map.put(START_DATE,start);
|
||||||
map.put(DUE_DATE,dueDate);
|
map.put(DUE_DATE,dueDate);
|
||||||
@@ -254,6 +253,11 @@ public class Task implements Mappable {
|
|||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> toMap() {
|
||||||
|
return toMap(true);
|
||||||
|
}
|
||||||
|
|
||||||
private int totalPrio() {
|
private int totalPrio() {
|
||||||
if (status >= Status.COMPLETE.code()) return 0; // task is done, do no longer highlight
|
if (status >= Status.COMPLETE.code()) return 0; // task is done, do no longer highlight
|
||||||
int total = priority;
|
int total = priority;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
import { api } from '../../urls.svelte';
|
import { api, eventStream } from '../../urls.svelte';
|
||||||
import { error, yikes } from '../../warn.svelte';
|
import { error, yikes } from '../../warn.svelte';
|
||||||
import { t } from '../../translations.svelte';
|
import { t } from '../../translations.svelte';
|
||||||
|
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
import Template from './Template.svelte';
|
import Template from './Template.svelte';
|
||||||
|
|
||||||
let bookmarks = $state(null);
|
let bookmarks = $state(null);
|
||||||
|
let eventSource = null;
|
||||||
let loader = {
|
let loader = {
|
||||||
offset : 0,
|
offset : 0,
|
||||||
limit : 16,
|
limit : 16,
|
||||||
@@ -45,6 +46,27 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleCreateEvent(evt){
|
||||||
|
let data = JSON.parse(evt.data);
|
||||||
|
if (data.record) bookmarks = [data.record, ...bookmarks];
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeleteEvent(evt){
|
||||||
|
let data = JSON.parse(evt.data);
|
||||||
|
if (data.record && data.record.id) bookmarks = bookmarks.filter(b => b.id != data.record.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleUpdateEvent(evt){
|
||||||
|
let data = JSON.parse(evt.data);
|
||||||
|
if (data.record && data.record.id) bookmarks = bookmarks.map(b => data.record.id == b.id ? data.record : b);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function load(){
|
||||||
|
loadBookmarks();
|
||||||
|
eventSource = eventStream(handleCreateEvent,handleUpdateEvent,handleDeleteEvent);
|
||||||
|
}
|
||||||
|
|
||||||
async function loadBookmarks(){
|
async function loadBookmarks(){
|
||||||
const url = api(`bookmark/list?offset=${loader.offset}&limit=${loader.limit}`);
|
const url = api(`bookmark/list?offset=${loader.offset}&limit=${loader.limit}`);
|
||||||
const resp = await fetch(url,{credentials:'include'});
|
const resp = await fetch(url,{credentials:'include'});
|
||||||
@@ -92,7 +114,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(loadBookmarks);
|
onMount(load);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
|
|||||||
@@ -1,19 +1,41 @@
|
|||||||
<script>
|
<script>
|
||||||
import { target } from '../../urls.svelte';
|
import { useTinyRouter } from 'svelte-tiny-router';
|
||||||
|
import { api, drop, target } from '../../urls.svelte';
|
||||||
|
import { error, yikes } from '../../warn.svelte';
|
||||||
|
import { t } from '../../translations.svelte';
|
||||||
import Tags from '../tags/TagList.svelte';
|
import Tags from '../tags/TagList.svelte';
|
||||||
|
|
||||||
|
const router = useTinyRouter();
|
||||||
let { bookmark } = $props();
|
let { bookmark } = $props();
|
||||||
|
|
||||||
|
async function del(bookmark){
|
||||||
|
if (confirm(t('confirm_delete',{element:bookmark.url}))){
|
||||||
|
var url = api(`bookmark/${bookmark.id}`)
|
||||||
|
var res = await drop(url);
|
||||||
|
if (res.ok){
|
||||||
|
yikes();
|
||||||
|
router.navigate('/bookmark')
|
||||||
|
} else error(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function edit(bookmark){
|
||||||
|
router.navigate(`/bookmark/${bookmark.id}/view`);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if bookmark}
|
{#if bookmark}
|
||||||
<fieldset class="bookmark">
|
<fieldset class="bookmark">
|
||||||
<legend>
|
<legend>
|
||||||
<a href={bookmark.url} target="_blank" class="url">{bookmark.url}</a>
|
<a href={bookmark.url} target="_blank" class="url">{bookmark.url}</a>
|
||||||
|
<a class="symbol" onclick={e => edit(bookmark)} title={t('edit_object',{object:t('bookmark')})} ></a>
|
||||||
|
<a class="symbol" onclick={e => del(bookmark)} title={t('delete_object',{object:t('bookmark')})} ></a>
|
||||||
</legend>
|
</legend>
|
||||||
<legend class="date">
|
<legend class="date">
|
||||||
{bookmark.timestamp.replace('T',' ')}
|
{bookmark.timestamp.replace('T',' ')}
|
||||||
</legend>
|
</legend>
|
||||||
{@html target(bookmark.comment.rendered)}
|
{@html target(bookmark.comment.rendered)}
|
||||||
<Tags module="bookmark" id={bookmark.id} />
|
<Tags module="bookmark" id={bookmark.id} />
|
||||||
|
<button onclick={e => edit(bookmark)} >{t('edit')}</button>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -2,8 +2,11 @@
|
|||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
import Bookmark from './Template.svelte';
|
import Bookmark from './Template.svelte';
|
||||||
|
import LineEditor from '../../Components/LineEditor.svelte';
|
||||||
|
import MarkdownEditor from '../../Components/MarkdownEditor.svelte';
|
||||||
|
import Tags from '../tags/TagList.svelte';
|
||||||
|
|
||||||
import { api } from '../../urls.svelte';
|
import { api, patch } from '../../urls.svelte';
|
||||||
import { error, yikes } from '../../warn.svelte';
|
import { error, yikes } from '../../warn.svelte';
|
||||||
import { t } from '../../translations.svelte';
|
import { t } from '../../translations.svelte';
|
||||||
|
|
||||||
@@ -24,7 +27,55 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function visit(ev){
|
||||||
|
window.open(bookmark.url, '_blank').focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function update(field,value){
|
||||||
|
var url = api(`bookmark/${id}`);
|
||||||
|
var res = await patch(url,{[field]:value});
|
||||||
|
if (res.ok){
|
||||||
|
yikes();
|
||||||
|
bookmark = await res.json();
|
||||||
|
if (id != bookmark.id){
|
||||||
|
id = bookmark.id;
|
||||||
|
history.pushState({}, null, `/bookmark/${id}/view`);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
error(res);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
onMount(load);
|
onMount(load);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Template {bookmark} />
|
{#if bookmark}
|
||||||
|
<fieldset>
|
||||||
|
<legend>{t('bookmark')} {id}</legend>
|
||||||
|
<table class="edit bookmark">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>{t('URL')}</th>
|
||||||
|
<td>
|
||||||
|
<LineEditor value={bookmark.url} editable={true} onSet={url => update('url',url)} />
|
||||||
|
<button onclick={visit}>{t('open')}</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{t('description')}</th>
|
||||||
|
<td>
|
||||||
|
<MarkdownEditor editable={true} value={bookmark.comment} onSet={desc => update('comment',desc)}/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{t('tags')}</th>
|
||||||
|
<td>
|
||||||
|
<Tags module="bookmark" id={bookmark.id} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</fieldset>
|
||||||
|
{/if}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import { useTinyRouter } from 'svelte-tiny-router';
|
import { useTinyRouter } from 'svelte-tiny-router';
|
||||||
|
|
||||||
import { api, eventStream, target } from '../../urls.svelte.js';
|
import { api, patch, post, eventStream, target } from '../../urls.svelte.js';
|
||||||
import { error, messages, yikes } from '../../warn.svelte';
|
import { error, messages, yikes } from '../../warn.svelte';
|
||||||
import { t } from '../../translations.svelte.js';
|
import { t } from '../../translations.svelte.js';
|
||||||
import { user } from '../../user.svelte.js';
|
import { user } from '../../user.svelte.js';
|
||||||
@@ -46,11 +46,7 @@
|
|||||||
}
|
}
|
||||||
task.members[user_id] = { permission: { name : 'ASSIGNEE' }};
|
task.members[user_id] = { permission: { name : 'ASSIGNEE' }};
|
||||||
task.members[user.id] = { permission: { name : 'OWNER' }};
|
task.members[user.id] = { permission: { name : 'OWNER' }};
|
||||||
const resp = await fetch(url,{
|
const resp = await post(url,task);
|
||||||
credentials : 'include',
|
|
||||||
method : 'POST',
|
|
||||||
body : JSON.stringify(task)
|
|
||||||
});
|
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
task = await resp.json();
|
task = await resp.json();
|
||||||
task.assignee = user_id;
|
task.assignee = user_id;
|
||||||
@@ -69,11 +65,7 @@
|
|||||||
ex.preventDefault();
|
ex.preventDefault();
|
||||||
var task = dragged;
|
var task = dragged;
|
||||||
const url = api(`task/${task.id}`);
|
const url = api(`task/${task.id}`);
|
||||||
const resp = await fetch(url,{
|
const resp = await patch(url,{no_index:true});
|
||||||
credentials : 'include',
|
|
||||||
method : 'PATCH',
|
|
||||||
body : JSON.stringify({no_index:true})
|
|
||||||
});
|
|
||||||
delete highlight.archive;
|
delete highlight.archive;
|
||||||
if (resp.ok){
|
if (resp.ok){
|
||||||
yikes();
|
yikes();
|
||||||
@@ -89,14 +81,10 @@
|
|||||||
highlight = {};
|
highlight = {};
|
||||||
|
|
||||||
if (task.assignee == user_id && task.status == state) return; // no change
|
if (task.assignee == user_id && task.status == state) return; // no change
|
||||||
let patch = {members:{},status:+state}
|
let data = {members:{},status:+state}
|
||||||
patch.members[user_id] = 'ASSIGNEE';
|
data.members[user_id] = 'ASSIGNEE';
|
||||||
const url = api(`task/${task.id}`);
|
const url = api(`task/${task.id}`);
|
||||||
const resp = await fetch(url,{
|
const resp = await patch(url,data);
|
||||||
credentials : 'include',
|
|
||||||
method : 'PATCH',
|
|
||||||
body : JSON.stringify(patch)
|
|
||||||
});
|
|
||||||
if (resp.ok){
|
if (resp.ok){
|
||||||
yikes();
|
yikes();
|
||||||
} else {
|
} else {
|
||||||
@@ -190,11 +178,8 @@
|
|||||||
const url = api('task/list');
|
const url = api('task/list');
|
||||||
selector.show_closed = true;
|
selector.show_closed = true;
|
||||||
selector.no_index = true;
|
selector.no_index = true;
|
||||||
var resp = await fetch(url,{
|
selector.rendered = false;
|
||||||
credentials :'include',
|
var resp = await post(url,selector);
|
||||||
method : 'POST',
|
|
||||||
body : JSON.stringify(selector)
|
|
||||||
});
|
|
||||||
if (resp.ok){
|
if (resp.ok){
|
||||||
var json = await resp.json();
|
var json = await resp.json();
|
||||||
for (var task_id of Object.keys(json)) {
|
for (var task_id of Object.keys(json)) {
|
||||||
@@ -239,11 +224,7 @@
|
|||||||
share : user_ids
|
share : user_ids
|
||||||
}
|
}
|
||||||
const url = api('bookmark');
|
const url = api('bookmark');
|
||||||
const resp = await fetch(url,{
|
const resp = await post(url,data);
|
||||||
credentials : 'include',
|
|
||||||
method : 'POST',
|
|
||||||
body : JSON.stringify(data)
|
|
||||||
});
|
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
yikes();
|
yikes();
|
||||||
router.navigate('/bookmark');
|
router.navigate('/bookmark');
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { useTinyRouter } from 'svelte-tiny-router';
|
import { useTinyRouter } from 'svelte-tiny-router';
|
||||||
|
|
||||||
import { api, get, patch } from '../../urls.svelte.js';
|
import { api, eventStream, get, patch } from '../../urls.svelte.js';
|
||||||
import { error, yikes } from '../../warn.svelte';
|
import { error, yikes } from '../../warn.svelte';
|
||||||
import { t } from '../../translations.svelte.js';
|
import { t } from '../../translations.svelte.js';
|
||||||
|
|
||||||
|
|||||||
@@ -218,10 +218,16 @@ CREATE TABLE IF NOT EXISTS {0} (
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void deleteEntity(String module, long entityId) {
|
public void deleteEntity(String module, long entityId) {
|
||||||
|
deleteEntity(module,entityId,-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteEntity(String module, long entityId, long userId) {
|
||||||
try {
|
try {
|
||||||
Query.delete().from(TABLE_TAGS)
|
var query = Query.delete().from(TABLE_TAGS)
|
||||||
.where(MODULE,iEqual(module)).where(ENTITY_ID,equal(entityId))
|
.where(MODULE,iEqual(module)).where(ENTITY_ID,equal(entityId));
|
||||||
.execute(db);
|
if (userId>0) query.where(USER_ID,equal(userId));
|
||||||
|
query.execute(db);
|
||||||
} catch (SQLException e){
|
} catch (SQLException e){
|
||||||
throw failedToDropObject(Translatable.t("{module}.{id}", MODULE,module, ID,entityId)).causedBy(e);
|
throw failedToDropObject(Translatable.t("{module}.{id}", MODULE,module, ID,entityId)).causedBy(e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ public interface TagDB {
|
|||||||
|
|
||||||
void deleteEntity(String module, long entityId);
|
void deleteEntity(String module, long entityId);
|
||||||
|
|
||||||
|
void deleteEntity(String module, long entityId, long userId);
|
||||||
|
|
||||||
Map<String, List<Long>> getUses(String tag, long id);
|
Map<String, List<Long>> getUses(String tag, long id);
|
||||||
|
|
||||||
Collection<Tuple<String, Long>> list(long userId);
|
Collection<Tuple<String, Long>> list(long userId);
|
||||||
|
|||||||
@@ -134,6 +134,11 @@ public class TagModule extends BaseHandler implements TagService {
|
|||||||
return sendContent(ex,tuples.map(t -> Map.of(TAG,t.a,COUNT,t.b)));
|
return sendContent(ex,tuples.map(t -> Map.of(TAG,t.a,COUNT,t.b)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteEntity(String module, long entityId, long userId) {
|
||||||
|
tagDb.deleteEntity(module,entityId,userId);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void save(String module, long entityId, Collection<Long> userIds, Collection<String> tags) {
|
public void save(String module, long entityId, Collection<Long> userIds, Collection<String> tags) {
|
||||||
tagDb.save(userIds,module,entityId,tags);
|
tagDb.save(userIds,module,entityId,tags);
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import de.srsoftware.tools.SessionToken;
|
|||||||
import de.srsoftware.umbrella.core.BaseHandler;
|
import de.srsoftware.umbrella.core.BaseHandler;
|
||||||
import de.srsoftware.umbrella.core.ModuleRegistry;
|
import de.srsoftware.umbrella.core.ModuleRegistry;
|
||||||
import de.srsoftware.umbrella.core.api.*;
|
import de.srsoftware.umbrella.core.api.*;
|
||||||
|
import de.srsoftware.umbrella.core.constants.Field;
|
||||||
import de.srsoftware.umbrella.core.constants.Text;
|
import de.srsoftware.umbrella.core.constants.Text;
|
||||||
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
|
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
|
||||||
import de.srsoftware.umbrella.core.model.*;
|
import de.srsoftware.umbrella.core.model.*;
|
||||||
@@ -271,6 +272,12 @@ public class TaskModule extends BaseHandler implements TaskService {
|
|||||||
return taskList;
|
return taskList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Map<Long,Map<String,Object>> mapTasks(Map<Long,Task> tasks, boolean render){
|
||||||
|
if (render) return mapValues(tasks);
|
||||||
|
return tasks.entrySet().stream()
|
||||||
|
.collect(Collectors.toMap(Map.Entry::getKey,e -> e.getValue().toMap(false)));
|
||||||
|
}
|
||||||
|
|
||||||
private boolean newParentIsSubtask(Task task, long newParent) {
|
private boolean newParentIsSubtask(Task task, long newParent) {
|
||||||
var parent = taskDb.load(newParent);
|
var parent = taskDb.load(newParent);
|
||||||
while (parent != null) {
|
while (parent != null) {
|
||||||
@@ -422,21 +429,23 @@ public class TaskModule extends BaseHandler implements TaskService {
|
|||||||
var noIndex = json.has(NO_INDEX) && json.get(NO_INDEX) instanceof Boolean bool ? bool : false;
|
var noIndex = json.has(NO_INDEX) && json.get(NO_INDEX) instanceof Boolean bool ? bool : false;
|
||||||
var projectId = json.has(PROJECT_ID) && json.get(PROJECT_ID) instanceof Number number ? number.longValue() : null;
|
var projectId = json.has(PROJECT_ID) && json.get(PROJECT_ID) instanceof Number number ? number.longValue() : null;
|
||||||
var parentTaskId = json.has(PARENT_TASK_ID) && json.get(PARENT_TASK_ID) instanceof Number number ? number.longValue() : null;
|
var parentTaskId = json.has(PARENT_TASK_ID) && json.get(PARENT_TASK_ID) instanceof Number number ? number.longValue() : null;
|
||||||
|
var markdown = !json.has(RENDERED) || !(json.get(RENDERED) instanceof Boolean render) || render;
|
||||||
if (isSet(projectId)) {
|
if (isSet(projectId)) {
|
||||||
if (parentTaskId == null) {
|
if (parentTaskId == null) {
|
||||||
var list = taskDb.listRootTasks(projectId, user, showClosed);
|
var list = taskDb.listRootTasks(projectId, user, showClosed);
|
||||||
return sendContent(ex, mapValues(list));
|
return sendContent(ex, mapTasks(list,markdown));
|
||||||
}
|
}
|
||||||
var projectTasks = taskDb.listProjectTasks(projectId, parentTaskId, noIndex);
|
var projectTasks = taskDb.listProjectTasks(projectId, parentTaskId, noIndex);
|
||||||
loadMembers(projectTasks.values());
|
loadMembers(projectTasks.values());
|
||||||
var tags = tagService().getTags(TASK,projectTasks.keySet(),user);
|
var tags = tagService().getTags(TASK,projectTasks.keySet(),user);
|
||||||
|
|
||||||
projectTasks = addTags(projectTasks, tags);
|
projectTasks = addTags(projectTasks, tags);
|
||||||
return sendContent(ex, mapValues(projectTasks));
|
return sendContent(ex, mapTasks(projectTasks, markdown));
|
||||||
}
|
}
|
||||||
if (isSet(parentTaskId)) return sendContent(ex, mapValues(taskDb.listChildrenOf(parentTaskId, user, showClosed)));
|
if (isSet(parentTaskId)) return sendContent(ex, mapValues(taskDb.listChildrenOf(parentTaskId, user, showClosed)));
|
||||||
var taskIds = json.has(IDS) && json.get(IDS) instanceof JSONArray ids ? ids.toList().stream().map(Object::toString).map(Long::parseLong).toList() : null;
|
var taskIds = json.has(IDS) && json.get(IDS) instanceof JSONArray ids ? ids.toList().stream().map(Object::toString).map(Long::parseLong).toList() : null;
|
||||||
if (isSet(taskIds)) return sendContent(ex, mapValues(taskDb.load(taskIds)));
|
var tasks = taskDb.load(taskIds);
|
||||||
|
if (isSet(taskIds)) return sendContent(ex, mapTasks(tasks,markdown));
|
||||||
return sendEmptyResponse(HTTP_NOT_IMPLEMENTED, ex);
|
return sendEmptyResponse(HTTP_NOT_IMPLEMENTED, ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user