Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 22094e7ccc | |||
| 3927898c9b | |||
| 2fcf024410 | |||
| 5c0efe5730 | |||
| 72f897c40c | |||
| d3e5897cd5 | |||
| ef71cf3b20 | |||
| e14e37fc9d | |||
| 410065f712 | |||
| 3e6ee91041 | |||
| 9a84fa6bc6 | |||
| 2d6b017352 | |||
| b71fd4492c | |||
| eede073cff | |||
| e4a9463307 | |||
| b73fb7c716 |
@@ -59,6 +59,7 @@ tasks.jar {
|
||||
":markdown:jar",
|
||||
":messages:jar",
|
||||
":notes:jar",
|
||||
":poll:jar",
|
||||
":project:jar",
|
||||
":stock:jar",
|
||||
":tags:jar",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
description = "Umbrella : Bookmarks"
|
||||
|
||||
dependencies{
|
||||
implementation(project(":bus"))
|
||||
implementation(project(":core"))
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package de.srsoftware.umbrella.bookmarks;
|
||||
|
||||
import static de.srsoftware.umbrella.bookmarks.Constants.*;
|
||||
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.constants.Field.*;
|
||||
import static de.srsoftware.umbrella.core.constants.Field.TAGS;
|
||||
@@ -10,7 +11,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.SEARCH;
|
||||
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_OK;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import de.srsoftware.configuration.Configuration;
|
||||
@@ -19,14 +24,13 @@ import de.srsoftware.tools.SessionToken;
|
||||
import de.srsoftware.umbrella.core.BaseHandler;
|
||||
import de.srsoftware.umbrella.core.ModuleRegistry;
|
||||
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.model.Token;
|
||||
import de.srsoftware.umbrella.core.model.UmbrellaUser;
|
||||
import de.srsoftware.umbrella.messagebus.events.BookmarkEvent;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.*;
|
||||
import org.json.JSONArray;
|
||||
|
||||
public class BookmarkApi extends BaseHandler implements BookmarkService {
|
||||
@@ -39,6 +43,30 @@ public class BookmarkApi extends BaseHandler implements BookmarkService {
|
||||
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
|
||||
public boolean doGet(Path path, HttpExchange ex) throws IOException {
|
||||
addCors(ex);
|
||||
@@ -51,21 +79,33 @@ public class BookmarkApi extends BaseHandler implements BookmarkService {
|
||||
case LIST -> getUserBookmarks(user.get(),ex);
|
||||
case null -> super.doPost(path,ex);
|
||||
default -> {
|
||||
var id = Long.parseLong(head);
|
||||
yield getBookmark(user.get(),id,ex);
|
||||
var urlId = Long.parseLong(head);
|
||||
yield getBookmark(user.get(),urlId,ex);
|
||||
}
|
||||
};
|
||||
} catch (NumberFormatException e){
|
||||
return sendContent(ex,HTTP_BAD_REQUEST,"Invalid project id");
|
||||
throw invalidField(ID, Text.NUMBER);
|
||||
} catch (UmbrellaException e){
|
||||
return send(ex,e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean getBookmark(UmbrellaUser user, long id, HttpExchange ex) throws IOException {
|
||||
var bookmark = db.load(id,user.id());
|
||||
ModuleRegistry.tagService().getTags(BOOKMARK, id, user).forEach(bookmark.tags()::add);
|
||||
return sendContent(ex,bookmark);
|
||||
@Override
|
||||
public boolean doPatch(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 patchBookmark(user.get(),ex,id);
|
||||
} catch (NumberFormatException e){
|
||||
throw invalidField(ID, Text.NUMBER);
|
||||
} catch (UmbrellaException e){
|
||||
return send(ex,e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -93,6 +133,12 @@ public class BookmarkApi extends BaseHandler implements BookmarkService {
|
||||
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 {
|
||||
var param = queryParam(ex);
|
||||
long offset = switch (param.get(OFFSET)){
|
||||
@@ -110,6 +156,23 @@ public class BookmarkApi extends BaseHandler implements BookmarkService {
|
||||
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 {
|
||||
var json = json(ex);
|
||||
if (!(json.has(URL) && json.get(URL) instanceof String url)) throw missingField(URL);
|
||||
@@ -123,10 +186,11 @@ public class BookmarkApi extends BaseHandler implements BookmarkService {
|
||||
}
|
||||
}
|
||||
var bookmark = db.save(url,comment, userList);
|
||||
messageBus().dispatch(new BookmarkEvent(user,bookmark,CREATE));
|
||||
|
||||
if (json.has(TAGS) && json.get(TAGS) instanceof JSONArray tagList){
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
package de.srsoftware.umbrella.bookmarks;
|
||||
|
||||
import de.srsoftware.umbrella.core.model.Bookmark;
|
||||
import de.srsoftware.umbrella.core.model.UmbrellaUser;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
@@ -15,6 +16,8 @@ public interface BookmarkDb {
|
||||
|
||||
Bookmark load(long id, long userId);
|
||||
|
||||
void remove(UmbrellaUser user, Bookmark bookmark);
|
||||
|
||||
Bookmark save(String url, String comment, Collection<Long> userIds, LocalDateTime datetime);
|
||||
|
||||
default Bookmark save(String url, String comment, Collection<Long> userIds){
|
||||
|
||||
@@ -7,15 +7,21 @@ import static de.srsoftware.tools.jdbc.Query.*;
|
||||
import static de.srsoftware.tools.jdbc.Query.SelectQuery.ALL;
|
||||
import static de.srsoftware.umbrella.bookmarks.Constants.*;
|
||||
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.Text.BOOKMARK;
|
||||
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*;
|
||||
import static de.srsoftware.umbrella.messagebus.MessageBus.messageBus;
|
||||
import static java.text.MessageFormat.format;
|
||||
import static java.time.ZoneOffset.UTC;
|
||||
|
||||
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.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.SQLException;
|
||||
import java.time.LocalDateTime;
|
||||
@@ -117,16 +123,29 @@ CREATE TABLE IF NOT EXISTS {0} (
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bookmark load(long id, long userId) {
|
||||
public Bookmark load(long urlId, long userId) {
|
||||
try {
|
||||
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);
|
||||
rs.close();
|
||||
if (result != null) return result;
|
||||
throw failedToLoadObject(Translatable.t(BOOKMARK),id);
|
||||
throw failedToLoadObject(Translatable.t(BOOKMARK),urlId);
|
||||
} 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,40 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.messagebus.events;
|
||||
|
||||
import static de.srsoftware.umbrella.core.constants.Module.BOOKMARK;
|
||||
import static de.srsoftware.umbrella.core.model.Translatable.t;
|
||||
|
||||
import de.srsoftware.umbrella.core.constants.Field;
|
||||
import de.srsoftware.umbrella.core.model.Bookmark;
|
||||
import de.srsoftware.umbrella.core.model.Translatable;
|
||||
import de.srsoftware.umbrella.core.model.UmbrellaUser;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.messagebus.events;
|
||||
|
||||
import de.srsoftware.umbrella.core.ModuleRegistry;
|
||||
import de.srsoftware.umbrella.core.api.Owner;
|
||||
import de.srsoftware.umbrella.core.constants.Field;
|
||||
import de.srsoftware.umbrella.core.model.*;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import static de.srsoftware.umbrella.core.constants.Field.*;
|
||||
import static de.srsoftware.umbrella.core.model.Translatable.t;
|
||||
|
||||
import de.srsoftware.umbrella.core.ModuleRegistry;
|
||||
import de.srsoftware.umbrella.core.api.Owner;
|
||||
import de.srsoftware.umbrella.core.model.*;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
public class ItemEvent extends Event<Item>{
|
||||
public ItemEvent(UmbrellaUser initiator, String module, Item item, EventType type) {
|
||||
super(initiator, module, item, type);
|
||||
|
||||
@@ -8,7 +8,9 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
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);
|
||||
|
||||
|
||||
@@ -150,6 +150,7 @@ public class Field {
|
||||
public static final String SUBJECT = "subject";
|
||||
|
||||
public static final String TABLE = "table";
|
||||
public static final String TAG = "tag";
|
||||
public static final String TAGS = "tags";
|
||||
public static final String TAG_COLORS = "tag_colors";
|
||||
public static final String TASK = "task";
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.core.model;
|
||||
|
||||
import static de.srsoftware.umbrella.core.constants.Field.*;
|
||||
import static java.text.MessageFormat.format;
|
||||
|
||||
import de.srsoftware.tools.Mappable;
|
||||
import de.srsoftware.umbrella.core.ModuleRegistry;
|
||||
import de.srsoftware.umbrella.core.Util;
|
||||
import de.srsoftware.umbrella.core.api.Owner;
|
||||
import de.srsoftware.umbrella.core.constants.Field;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static de.srsoftware.umbrella.core.constants.Field.*;
|
||||
import static java.text.MessageFormat.format;
|
||||
|
||||
public class Poll implements Mappable {
|
||||
public static class Option implements Mappable{
|
||||
|
||||
|
||||
@@ -228,20 +228,19 @@ public class Task implements Mappable {
|
||||
return tags;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> toMap() {
|
||||
public Map<String,Object> toMap(boolean renderMarkdown){
|
||||
var map = new HashMap<String,Object>();
|
||||
var memberMap = new HashMap<Long,Map<String,Object>>();
|
||||
if (members != null) for (var entry : members.entrySet()){
|
||||
memberMap.put(entry.getKey(),entry.getValue().toMap());
|
||||
}
|
||||
map.put(ID, id);
|
||||
map.put(PROJECT_ID, projectId);
|
||||
map.put(PARENT_TASK_ID, parentTaskId);
|
||||
map.put(PRIORITY,priority);
|
||||
map.put(NAME, name);
|
||||
map.put(DESCRIPTION, mapMarkdown(description));
|
||||
map.put(STATUS, status);
|
||||
map.put(PROJECT_ID, projectId);
|
||||
map.put(PARENT_TASK_ID, parentTaskId);
|
||||
map.put(PRIORITY,priority);
|
||||
map.put(NAME, name);
|
||||
map.put(DESCRIPTION, renderMarkdown ? mapMarkdown(description) : Map.of(SOURCE,description));
|
||||
map.put(STATUS, status);
|
||||
map.put(EST_TIME, estimatedTime);
|
||||
map.put(START_DATE,start);
|
||||
map.put(DUE_DATE,dueDate);
|
||||
@@ -254,6 +253,11 @@ public class Task implements Mappable {
|
||||
return map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> toMap() {
|
||||
return toMap(true);
|
||||
}
|
||||
|
||||
private int totalPrio() {
|
||||
if (status >= Status.COMPLETE.code()) return 0; // task is done, do no longer highlight
|
||||
int total = priority;
|
||||
|
||||
@@ -2,59 +2,118 @@
|
||||
import { t } from '../translations.svelte.js'
|
||||
import { tick } from "svelte";
|
||||
|
||||
|
||||
let {
|
||||
getCandidates = async text => { conole.log('no handler for getCandidates('+text+')'); return {};},
|
||||
onSelect = text => []
|
||||
autofocus = false,
|
||||
getCandidates = dummyGetCandidates,
|
||||
onCommit = dummyOnCommit,
|
||||
onSelect = dummyOnSelect,
|
||||
candidate = $bindable({ display : '' })
|
||||
} = $props();
|
||||
|
||||
const ignore = ['Escape','Tab','ArrowUp','ArrowLeft','ArrowRight']
|
||||
let options = $state({});
|
||||
let text = $state('')
|
||||
const ignore = ['ArrowLeft','ArrowRight'];
|
||||
//let candidate = $state({ display : '' });
|
||||
let selected = $state([]);
|
||||
let candidates = $derived(getCandidates(candidate.display));
|
||||
|
||||
async function dummyGetCandidates(text){
|
||||
console.warn(`getCandidates(${text}) not overridden!`);
|
||||
if (!text) return [];
|
||||
return [
|
||||
{
|
||||
display : 'candidate 1',
|
||||
explanation : 'candidates need to have a display field'
|
||||
},
|
||||
{
|
||||
display : 'candidate 2',
|
||||
additional : 'other fields are optional',
|
||||
more : 'and may be domain specific'
|
||||
},
|
||||
{ display : text }
|
||||
];
|
||||
}
|
||||
|
||||
function dummyOnCommit(candidate){
|
||||
// if Enter is pressed on the input field, this method gets called with
|
||||
// either the selected candidate or
|
||||
// an anonymous object with the entered text in the display field
|
||||
console.warn(`onCommit(${JSON.stringify(candidate)}) not overridden!`);
|
||||
// if the method returns true, the field will be cleared after committing
|
||||
return true;
|
||||
}
|
||||
|
||||
function dummyOnSelect(candidate){
|
||||
console.warn(`${candidate.display} selected, but onSelect not overridden!`)
|
||||
}
|
||||
|
||||
async function ondblclick(evt){
|
||||
const select = evt.target;
|
||||
const key = select.value;
|
||||
text = options[key];
|
||||
let result = {};
|
||||
result[key] = text;
|
||||
options = {};
|
||||
text = '';
|
||||
onSelect(result);
|
||||
const idx = select.value;
|
||||
candidate = candidates[idx];
|
||||
candidates = [];
|
||||
selected = [];
|
||||
console.log(candidate);
|
||||
onSelect(candidate);
|
||||
}
|
||||
|
||||
async function onkeyup(evt){
|
||||
const select = evt.target;
|
||||
const key = evt.key;
|
||||
if (ignore.includes(key)) return;
|
||||
if (key == 'ArrowDown'){
|
||||
if (select.selectedIndex == 0) select.selectedIndex=1;
|
||||
return;
|
||||
async function onkeyup(ev){
|
||||
if (ignore.includes(ev.key)) return;
|
||||
if (ev.key == 'ArrowDown'){
|
||||
ev.preventDefault();
|
||||
selected = selected.length < 1 ? [0] : [selected[0]+1]
|
||||
if (selected[0] >= candidates.length) selected = [0];
|
||||
return false;
|
||||
}
|
||||
if (key == 'Enter'){
|
||||
ondblclick(evt);
|
||||
return;
|
||||
if (ev.key == 'ArrowUp'){
|
||||
ev.preventDefault();
|
||||
selected = selected.length < 1 ? [-1] : [selected[0]-1]
|
||||
if (selected[0] < 0) selected = [candidates.length-1];
|
||||
return false;
|
||||
}
|
||||
if (key == 'Backspace'){
|
||||
text = text.substring(0,text.length-1)
|
||||
} else if (key.length<2){
|
||||
text += evt.key
|
||||
if (ev.key == 'Enter'|| ev.key == 'Tab'){
|
||||
ev.preventDefault();
|
||||
if (selected.length>0) {
|
||||
candidate = candidates[selected[0]];
|
||||
candidates = [];
|
||||
selected = [];
|
||||
onSelect(candidate);
|
||||
return false;
|
||||
}
|
||||
if (ev.key == 'Enter') {
|
||||
candidates = [];
|
||||
selected = [];
|
||||
if (onCommit(candidate)) candidate = { display : '' };
|
||||
}
|
||||
return false;
|
||||
}
|
||||
options = await getCandidates(text);
|
||||
await tick();
|
||||
for (let o of select.getElementsByTagName('option')) o.selected = false;
|
||||
if (ev.key == 'Escape'){
|
||||
ev.preventDefault();
|
||||
candidates = [];
|
||||
selected = [];
|
||||
return false;
|
||||
}
|
||||
|
||||
candidates = await getCandidates(candidate.display);
|
||||
if (selected>candidates.length) selected = candidates.length;
|
||||
return false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
select{
|
||||
min-width: 200px;
|
||||
}
|
||||
span { position : relative }
|
||||
select { position : absolute; top: 30px; left: 3px; }
|
||||
|
||||
select { background: black; color: orange; border: 1px solid orange; border-radius: 5px; }
|
||||
option:checked { background: orange; color: black; }
|
||||
</style>
|
||||
{#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}
|
||||
|
||||
<span>
|
||||
<input type="text" bind:value={candidate.display} {onkeyup} autofocus={autofocus} />
|
||||
{#if candidates && candidates.length > 0}
|
||||
<select bind:value={selected} {ondblclick} multiple tabindex="-1">
|
||||
{#each candidates as candidate,i}
|
||||
<option value={i}>{candidate.display}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/if}
|
||||
</span>
|
||||
@@ -1,8 +1,9 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
import { api } from '../urls.svelte';
|
||||
import { api, get, post } from '../urls.svelte';
|
||||
import { t } from '../translations.svelte';
|
||||
import { error, yikes } from '../warn.svelte';
|
||||
|
||||
import Autocomplete from './Autocomplete.svelte';
|
||||
import PermissionSelector from './PermissionSelector.svelte';
|
||||
@@ -27,7 +28,7 @@
|
||||
});
|
||||
if (resp.ok){
|
||||
var json = await resp.json();
|
||||
return Object.fromEntries(Object.values(json).map(user => [user.id,user.name]));
|
||||
return Object.values(json).map(user => { return {...user,display:user.name}; });
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
@@ -35,16 +36,10 @@
|
||||
|
||||
async function loadPermissions(){
|
||||
const url = api('task/permissions');
|
||||
const resp = await fetch(url,{credentials: 'include'});
|
||||
const resp = await get(url);
|
||||
if (resp.ok){
|
||||
permissions = await resp.json();
|
||||
} else {
|
||||
message = await resp.text();
|
||||
}
|
||||
}
|
||||
|
||||
function onSelect(entry){
|
||||
addMember(entry);
|
||||
} else error(resp);
|
||||
}
|
||||
|
||||
onMount(loadPermissions);
|
||||
@@ -66,7 +61,7 @@
|
||||
<tr>
|
||||
<td>{t('add_object',{object:t('member')})}</td>
|
||||
<td>
|
||||
<Autocomplete {getCandidates} {onSelect} />
|
||||
<Autocomplete {getCandidates} onSelect={addMember} />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -16,9 +16,7 @@
|
||||
}
|
||||
|
||||
function onSelect(entry){
|
||||
for (let [k,v] of Object.entries(entry)){
|
||||
users[k] = {name:v,id:k};
|
||||
}
|
||||
users[entry.id] = entry;
|
||||
}
|
||||
|
||||
let sortedUsers = $derived.by(() => Object.values(users).sort((a, b) => a.name.localeCompare(b.name)));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
import { api } from '../../urls.svelte';
|
||||
import { api, eventStream } from '../../urls.svelte';
|
||||
import { error, yikes } from '../../warn.svelte';
|
||||
import { t } from '../../translations.svelte';
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import Template from './Template.svelte';
|
||||
|
||||
let bookmarks = $state(null);
|
||||
let eventSource = null;
|
||||
let loader = {
|
||||
offset : 0,
|
||||
limit : 16,
|
||||
@@ -36,15 +37,34 @@
|
||||
if (resp.ok){
|
||||
yikes();
|
||||
const input = await resp.json();
|
||||
return Object.fromEntries(
|
||||
Object.entries(input).map(([key, value]) => [key, value.name])
|
||||
);
|
||||
return Object.values(input).map(user => {return {...user,display:user.name}});
|
||||
} else {
|
||||
error(resp);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
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(){
|
||||
const url = api(`bookmark/list?offset=${loader.offset}&limit=${loader.limit}`);
|
||||
const resp = await fetch(url,{credentials:'include'});
|
||||
@@ -92,7 +112,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
onMount(loadBookmarks);
|
||||
onMount(load);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
||||
@@ -1,19 +1,41 @@
|
||||
<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';
|
||||
|
||||
const router = useTinyRouter();
|
||||
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>
|
||||
|
||||
{#if bookmark}
|
||||
<fieldset class="bookmark">
|
||||
<legend>
|
||||
<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 class="date">
|
||||
{bookmark.timestamp.replace('T',' ')}
|
||||
</legend>
|
||||
{@html target(bookmark.comment.rendered)}
|
||||
<Tags module="bookmark" id={bookmark.id} />
|
||||
<button onclick={e => edit(bookmark)} >{t('edit')}</button>
|
||||
</fieldset>
|
||||
{/if}
|
||||
@@ -2,8 +2,11 @@
|
||||
import { onMount } from '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 { 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);
|
||||
</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}
|
||||
|
||||
@@ -22,9 +22,7 @@
|
||||
if (resp.ok){
|
||||
yikes();
|
||||
const input = await resp.json();
|
||||
return Object.fromEntries(
|
||||
Object.entries(input).map(([key, value]) => [key, value.name])
|
||||
);
|
||||
return Object.values(input).map(user => { return {...user, display: user.name}});
|
||||
} else {
|
||||
error(resp);
|
||||
return {};
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
let members = $state([]);
|
||||
|
||||
function addMember(member){
|
||||
for (let [id,name] of Object.entries(member)) update_permissions({user_id:+id,permission:4});
|
||||
update_permissions({user_id:+member.id,permission:4});
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { useTinyRouter } from 'svelte-tiny-router';
|
||||
|
||||
import { api, eventStream, target } from '../../urls.svelte.js';
|
||||
import { api, get, patch, post, eventStream, target } from '../../urls.svelte.js';
|
||||
import { error, messages, yikes } from '../../warn.svelte';
|
||||
import { t } from '../../translations.svelte.js';
|
||||
import { user } from '../../user.svelte.js';
|
||||
|
||||
import Autocomplete from '../../Components/Autocomplete.svelte';
|
||||
import Card from './KanbanCard.svelte';
|
||||
import LineEditor from '../../Components/LineEditor.svelte';
|
||||
import MarkdownEditor from '../../Components/MarkdownEditor.svelte';
|
||||
@@ -16,12 +17,12 @@
|
||||
let connectionStatus = 'disconnected';
|
||||
let { id } = $props();
|
||||
let descr = $state(false);
|
||||
let filter_input = $state('');
|
||||
let filter_input = $state({display:''});
|
||||
let router = useTinyRouter();
|
||||
if (router.hasQueryParam('filter')) filter_input = router.getQueryParam('filter');
|
||||
if (router.hasQueryParam('filter')) filter_input = {display:router.getQueryParam('filter')};
|
||||
let dragged = null;
|
||||
let highlight = $state({});
|
||||
let filter = $derived(filter_input.toLowerCase());
|
||||
let filter = $derived(filter_input.display.toLowerCase());
|
||||
let project = $state(null);
|
||||
let tasks = $state({});
|
||||
let users = [];
|
||||
@@ -29,7 +30,7 @@
|
||||
let info = $state(null);
|
||||
let task_form = $state(false);
|
||||
let stateList = {};
|
||||
$effect(() => updateUrl(filter_input));
|
||||
$effect(() => updateUrl(filter_input.display));
|
||||
|
||||
function byName(a,b) {
|
||||
return a.name.localeCompare(b.name);
|
||||
@@ -46,11 +47,7 @@
|
||||
}
|
||||
task.members[user_id] = { permission: { name : 'ASSIGNEE' }};
|
||||
task.members[user.id] = { permission: { name : 'OWNER' }};
|
||||
const resp = await fetch(url,{
|
||||
credentials : 'include',
|
||||
method : 'POST',
|
||||
body : JSON.stringify(task)
|
||||
});
|
||||
const resp = await post(url,task);
|
||||
if (resp.ok) {
|
||||
task = await resp.json();
|
||||
task.assignee = user_id;
|
||||
@@ -69,11 +66,7 @@
|
||||
ex.preventDefault();
|
||||
var task = dragged;
|
||||
const url = api(`task/${task.id}`);
|
||||
const resp = await fetch(url,{
|
||||
credentials : 'include',
|
||||
method : 'PATCH',
|
||||
body : JSON.stringify({no_index:true})
|
||||
});
|
||||
const resp = await patch(url,{no_index:true});
|
||||
delete highlight.archive;
|
||||
if (resp.ok){
|
||||
yikes();
|
||||
@@ -89,14 +82,10 @@
|
||||
highlight = {};
|
||||
|
||||
if (task.assignee == user_id && task.status == state) return; // no change
|
||||
let patch = {members:{},status:+state}
|
||||
patch.members[user_id] = 'ASSIGNEE';
|
||||
let data = {members:{},status:+state}
|
||||
data.members[user_id] = 'ASSIGNEE';
|
||||
const url = api(`task/${task.id}`);
|
||||
const resp = await fetch(url,{
|
||||
credentials : 'include',
|
||||
method : 'PATCH',
|
||||
body : JSON.stringify(patch)
|
||||
});
|
||||
const resp = await patch(url,data);
|
||||
if (resp.ok){
|
||||
yikes();
|
||||
} else {
|
||||
@@ -104,6 +93,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function getCandidates(input){
|
||||
if (!input || input.length <3) return [];
|
||||
const url = api(`tags/search/${encodeURI(input)}`);
|
||||
const res = await get(url);
|
||||
if (res.ok){
|
||||
yikes();
|
||||
const list = await res.json();
|
||||
return list.map(elem => {return {display:elem}});
|
||||
} else error(res);
|
||||
}
|
||||
|
||||
function handleCreateEvent(evt){
|
||||
handleEvent(evt,'create');
|
||||
}
|
||||
@@ -190,11 +190,8 @@
|
||||
const url = api('task/list');
|
||||
selector.show_closed = true;
|
||||
selector.no_index = true;
|
||||
var resp = await fetch(url,{
|
||||
credentials :'include',
|
||||
method : 'POST',
|
||||
body : JSON.stringify(selector)
|
||||
});
|
||||
selector.rendered = false;
|
||||
var resp = await post(url,selector);
|
||||
if (resp.ok){
|
||||
var json = await resp.json();
|
||||
for (var task_id of Object.keys(json)) {
|
||||
@@ -206,6 +203,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
function onCommit(){
|
||||
return false;
|
||||
}
|
||||
|
||||
function openTask(task_id){
|
||||
window.open(`/task/${task_id}/view`, '_blank').focus();
|
||||
}
|
||||
@@ -234,16 +235,12 @@
|
||||
const user_ids = Object.values(project.members).map(member => member.user.id);
|
||||
const data = {
|
||||
url : location.href,
|
||||
tags : ['Kanban', project.name, filter_input],
|
||||
comment : `${project.name}: ${filter_input}`,
|
||||
tags : ['Kanban', project.name, filter_input.display],
|
||||
comment : `${project.name}: ${filter_input.display}`,
|
||||
share : user_ids
|
||||
}
|
||||
const url = api('bookmark');
|
||||
const resp = await fetch(url,{
|
||||
credentials : 'include',
|
||||
method : 'POST',
|
||||
body : JSON.stringify(data)
|
||||
});
|
||||
const resp = await post(url,data);
|
||||
if (resp.ok) {
|
||||
yikes();
|
||||
router.navigate('/bookmark');
|
||||
@@ -258,7 +255,7 @@
|
||||
|
||||
function updateUrl(){
|
||||
let url = window.location.origin + window.location.pathname;
|
||||
if (filter_input) url += '?filter=' + encodeURI(filter_input);
|
||||
if (filter_input.display) url += '?filter=' + encodeURI(filter_input.display);
|
||||
window.history.replaceState(window.history.state, '', url);
|
||||
}
|
||||
|
||||
@@ -291,7 +288,7 @@
|
||||
</fieldset>
|
||||
<div class="kanban" style="display: grid; grid-template-columns: {`repeat(${columns}, auto)`}">
|
||||
<span class="filter">
|
||||
<input type="text" bind:value={filter_input} autofocus />
|
||||
<Autocomplete {getCandidates} bind:candidate={filter_input} {onCommit} autofocus={true} />
|
||||
{t('filter')}
|
||||
<button style="visibility:{filter_input ? 'visible' : 'hidden'}" onclick={save_bookmark}>
|
||||
<span class="symbol"></span> {t('save_object',{object:t('bookmark')})}
|
||||
|
||||
@@ -28,9 +28,8 @@
|
||||
let new_state = $state({code:null,name:null})
|
||||
let state_available=$derived(new_state.name && new_state.code && !project.allowed_states[new_state.code]);
|
||||
|
||||
async function addMember(entry){
|
||||
const ids = Object.keys(entry);
|
||||
if (ids) update({new_member:+ids.pop()});
|
||||
async function addMember(user){
|
||||
return await update({new_member:+user.id});
|
||||
}
|
||||
|
||||
async function addState(){
|
||||
@@ -67,21 +66,6 @@
|
||||
update({drop_member:member.user.id});
|
||||
}
|
||||
|
||||
async function getCandidates(text){
|
||||
const url = api('user/search');
|
||||
const resp = await fetch(url,{
|
||||
credentials : 'include',
|
||||
method : 'POST',
|
||||
body : text
|
||||
});
|
||||
if (resp.ok){
|
||||
var json = await resp.json();
|
||||
return Object.fromEntries(Object.values(json).map(user => [user.id,user.name]));
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function handleCreate(evt){
|
||||
let json = JSON.parse(evt.data);
|
||||
json.event = 'create';
|
||||
@@ -242,7 +226,7 @@
|
||||
</label>
|
||||
<div class="em">{t('members')}</div>
|
||||
<div class="em">
|
||||
<PermissionEditor members={project.members} {updatePermission} {addMember} {dropMember} {getCandidates} />
|
||||
<PermissionEditor members={project.members} {updatePermission} {addMember} {dropMember} />
|
||||
</div>
|
||||
{#if project.allowed_states}
|
||||
{#each Object.keys(project.allowed_states) as key,idx}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { useTinyRouter } from 'svelte-tiny-router';
|
||||
|
||||
import { api, get } from '../../urls.svelte';
|
||||
import { error, yikes } from '../../warn.svelte';
|
||||
import { t } from '../../translations.svelte';
|
||||
|
||||
let {module, id} = $props();
|
||||
let item = $state(null);
|
||||
let router = useTinyRouter();
|
||||
|
||||
function onclick(e){
|
||||
e.preventDefault();
|
||||
var target = e.target;
|
||||
while (target && !target.href) target=target.parentNode;
|
||||
let href = target.getAttribute('href');
|
||||
if (href) router.navigate(href);
|
||||
return false;
|
||||
}
|
||||
|
||||
async function loadItem(){
|
||||
let url = api(`stock/item/${id}`);
|
||||
let res = await get(url);
|
||||
if (res.ok) {
|
||||
yikes();
|
||||
item = await res.json();
|
||||
} else error();
|
||||
}
|
||||
|
||||
|
||||
onMount(loadItem);
|
||||
</script>
|
||||
|
||||
{#if item}
|
||||
<a href="/stock/{id}/view" {onclick} >({item.code}) {item.name}</a>
|
||||
{/if}
|
||||
@@ -6,6 +6,8 @@
|
||||
import { error, yikes } from '../../warn.svelte';
|
||||
import { t } from '../../translations.svelte.js';
|
||||
|
||||
import ItemDisplay from '../stock/display.svelte';
|
||||
|
||||
let { module, id } = $props();
|
||||
let object = $state(null);
|
||||
let router = useTinyRouter();
|
||||
@@ -44,7 +46,9 @@
|
||||
{:else if module=='wiki'}
|
||||
<span onclick={go}>{object.title}</span>
|
||||
{:else if module=='document'}
|
||||
<span onclick={go}>{t(object.type)} ${object.number} (${object.customer.name.split('\n')[0]})</span>
|
||||
<span onclick={go}>{t('type_'+object.type)} {object.number} ({object.customer.name.split('\n')[0]})</span>
|
||||
{:else if module=='stock'}
|
||||
<ItemDisplay {module} {id} />
|
||||
{:else}
|
||||
<span class="error">No display defined in Reference.svelte for entities of type {module}.</span>
|
||||
{/if}
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
import {onMount} from 'svelte';
|
||||
import { useTinyRouter } from 'svelte-tiny-router';
|
||||
|
||||
import { api } from '../../urls.svelte.js';
|
||||
import Autocomplete from '../../Components/Autocomplete.svelte';
|
||||
|
||||
import { api, get, post } from '../../urls.svelte.js';
|
||||
import { error, yikes } from '../../warn.svelte';
|
||||
import { t } from '../../translations.svelte.js';
|
||||
import { user } from '../../user.svelte.js'
|
||||
@@ -13,10 +15,9 @@
|
||||
tags = $bindable([]),
|
||||
user_list = [],
|
||||
} = $props();
|
||||
let newTag = $state('');
|
||||
let router = useTinyRouter();
|
||||
|
||||
async function addTag(){
|
||||
async function addTag(newTag){
|
||||
if (!newTag) return;
|
||||
if (!id) {
|
||||
// when creating elements, they don`t have an id, yet
|
||||
@@ -63,10 +64,21 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
async function getCandidates(input){
|
||||
if (!input || input.length <3) return [];
|
||||
const url = api(`tags/search/${encodeURI(input)}`);
|
||||
const res = await get(url);
|
||||
if (res.ok){
|
||||
yikes();
|
||||
const list = await res.json();
|
||||
return list.map(elem => {return {display:elem}});
|
||||
} else error(res);
|
||||
}
|
||||
|
||||
async function loadTags(entityId){
|
||||
if (!entityId) return; // when crating elements, they dont`t have an id, yet.
|
||||
const url = api(`tags/${module}/${entityId}`);
|
||||
const resp = await fetch(url,{credentials:'include'});
|
||||
const resp = await get(url);
|
||||
if (resp.ok) {
|
||||
tags = await resp.json();
|
||||
tags = tags.sort();
|
||||
@@ -75,13 +87,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function onCommit(wrapped){
|
||||
addTag(wrapped.display);
|
||||
}
|
||||
|
||||
function onSelect(dummy){}
|
||||
|
||||
function show(tag){
|
||||
router.navigate(`/tags/use/${tag}`);
|
||||
}
|
||||
|
||||
function typed(ev){
|
||||
if (ev.keyCode == 13) addTag();
|
||||
}
|
||||
|
||||
$effect(() => loadTags(id));
|
||||
</script>
|
||||
@@ -93,6 +108,6 @@
|
||||
</span>
|
||||
{/each}
|
||||
<span class="tag editor">
|
||||
<input type="text" bind:value={newTag} onkeyup={typed} />
|
||||
<Autocomplete {getCandidates} {onCommit} {onSelect} />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -12,6 +12,12 @@
|
||||
let router = useTinyRouter();
|
||||
let uses = $state(null);
|
||||
|
||||
function headline(module){
|
||||
if (module == 'stock') return t(module);
|
||||
if (module.endsWith('s')) return t(`${module}s`);
|
||||
return t(module);
|
||||
}
|
||||
|
||||
async function loadUses(){
|
||||
const url = api(`tags/uses/${tag}`);
|
||||
const resp = await fetch(url,{credentials:'include'});
|
||||
@@ -36,7 +42,7 @@
|
||||
</legend>
|
||||
{#if uses}
|
||||
{#each Object.entries(uses) as [module,ids]}
|
||||
<h2>{t(module.endsWith('s') ? module : `${module}s`)}</h2>
|
||||
<h2>{headline(module)}</h2>
|
||||
<ul>
|
||||
{#each ids as id}
|
||||
<li><Reference {module} {id} /></li>
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
let router = useTinyRouter();
|
||||
|
||||
function addMember(member){
|
||||
for (let uid of Object.keys(member)) task.members[uid] = project.members[uid];
|
||||
let uid = member.id;
|
||||
task.members[uid] = project.members[uid];
|
||||
}
|
||||
|
||||
function flat(json){
|
||||
@@ -44,10 +45,9 @@
|
||||
|
||||
async function getCandidates(text){
|
||||
const origin = parent_task ? parent_task.members : project.members;
|
||||
const candidates = Object.values(origin)
|
||||
return Object.values(origin)
|
||||
.filter(member => member.user.name.toLowerCase().includes(text.toLowerCase()))
|
||||
.map(member => [member.user.id,member.user.name]);
|
||||
return Object.fromEntries(candidates);
|
||||
.map(member => {return { ...member.user,display:member.user.name}});
|
||||
}
|
||||
|
||||
async function load(){
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
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 { t } from '../../translations.svelte.js';
|
||||
|
||||
|
||||
@@ -37,9 +37,8 @@
|
||||
router.navigate(`/task/${id}/add_subtask`);
|
||||
}
|
||||
|
||||
async function addMember(entry){
|
||||
const ids = Object.keys(entry);
|
||||
if (ids) update({new_member:+ids.pop()});
|
||||
async function addMember(newMember){
|
||||
return await update({new_member:+newMember.id});
|
||||
}
|
||||
|
||||
async function addTime(){
|
||||
@@ -64,10 +63,10 @@
|
||||
|
||||
async function getCandidates(text){
|
||||
const origin = task.parent ? task.parent.members : project.members;
|
||||
const candidates = Object.values(origin)
|
||||
return Object.values(origin)
|
||||
.filter(member => member.user.name.toLowerCase().includes(text.toLowerCase()))
|
||||
.map(member => [member.user.id,member.user.name]);
|
||||
return Object.fromEntries(candidates);
|
||||
.map(member => { return {...member.user,display:member.user.name}});
|
||||
|
||||
}
|
||||
|
||||
function gotoKanban(){
|
||||
@@ -177,6 +176,8 @@
|
||||
});
|
||||
if (resp.ok){
|
||||
yikes();
|
||||
let json = await resp.json();
|
||||
if (json.members) task.members = json.members;
|
||||
return true;
|
||||
} else {
|
||||
error(resp);
|
||||
|
||||
@@ -22,9 +22,8 @@
|
||||
|
||||
async function addMember(entry){
|
||||
let newMembers = JSON.parse(JSON.stringify(page.members));
|
||||
for (var id of Object.keys(entry)){
|
||||
if (!newMembers[id]) newMembers[id] = { permission : {name:'READ_ONLY'} };
|
||||
}
|
||||
let id = entry.id;
|
||||
if (!newMembers[id]) newMembers[id] = { permission : {name:'READ_ONLY'} };
|
||||
return patch({members:newMembers});
|
||||
}
|
||||
|
||||
@@ -74,7 +73,7 @@
|
||||
});
|
||||
if (resp.ok){
|
||||
var json = await resp.json();
|
||||
return Object.fromEntries(Object.values(json).filter(nonMember).map(user => [user.id,user.name]));
|
||||
return Object.values(json).filter(nonMember).map(user => { return {...user,display:user.name}});
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ package de.srsoftware.umbrella.poll;
|
||||
import de.srsoftware.umbrella.core.model.Permission;
|
||||
import de.srsoftware.umbrella.core.model.Poll;
|
||||
import de.srsoftware.umbrella.core.model.UmbrellaUser;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import static de.srsoftware.umbrella.poll.Constants.CONFIG_DATABASE;
|
||||
import static java.lang.System.Logger.Level.WARNING;
|
||||
import static java.net.HttpURLConnection.HTTP_OK;
|
||||
import static java.text.MessageFormat.format;
|
||||
import static de.srsoftware.umbrella.core.model.Permission.READ_ONLY;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import de.srsoftware.configuration.Configuration;
|
||||
import de.srsoftware.tools.Path;
|
||||
@@ -26,10 +26,9 @@ import de.srsoftware.umbrella.core.model.Permission;
|
||||
import de.srsoftware.umbrella.core.model.Poll;
|
||||
import de.srsoftware.umbrella.core.model.Token;
|
||||
import de.srsoftware.umbrella.core.model.UmbrellaUser;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class PollModule extends BaseHandler implements PollService {
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
|
||||
import de.srsoftware.umbrella.core.model.Permission;
|
||||
import de.srsoftware.umbrella.core.model.Poll;
|
||||
import de.srsoftware.umbrella.core.model.UmbrellaUser;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.util.*;
|
||||
|
||||
@@ -10,6 +10,7 @@ import static de.srsoftware.tools.jdbc.Query.SelectQuery.ALL;
|
||||
import static de.srsoftware.umbrella.bookmarks.Constants.*;
|
||||
import static de.srsoftware.umbrella.core.Errors.*;
|
||||
import static de.srsoftware.umbrella.core.constants.Field.*;
|
||||
import static de.srsoftware.umbrella.core.constants.Field.TAG;
|
||||
import static de.srsoftware.umbrella.core.constants.Field.TAGS;
|
||||
import static de.srsoftware.umbrella.core.constants.Module.BOOKMARK;
|
||||
import static de.srsoftware.umbrella.core.constants.Text.TABLE_WITH_NAME;
|
||||
@@ -25,8 +26,10 @@ import de.srsoftware.tools.Tuple;
|
||||
import de.srsoftware.tools.jdbc.Query;
|
||||
import de.srsoftware.umbrella.bookmarks.BookmarkDb;
|
||||
import de.srsoftware.umbrella.core.BaseDb;
|
||||
import de.srsoftware.umbrella.core.constants.Field;
|
||||
import de.srsoftware.umbrella.core.constants.Text;
|
||||
import de.srsoftware.umbrella.core.model.Translatable;
|
||||
import de.srsoftware.umbrella.core.model.UmbrellaUser;
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.time.LocalDateTime;
|
||||
@@ -218,10 +221,16 @@ CREATE TABLE IF NOT EXISTS {0} (
|
||||
|
||||
@Override
|
||||
public void deleteEntity(String module, long entityId) {
|
||||
deleteEntity(module,entityId,-1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteEntity(String module, long entityId, long userId) {
|
||||
try {
|
||||
Query.delete().from(TABLE_TAGS)
|
||||
.where(MODULE,iEqual(module)).where(ENTITY_ID,equal(entityId))
|
||||
.execute(db);
|
||||
var query = Query.delete().from(TABLE_TAGS)
|
||||
.where(MODULE,iEqual(module)).where(ENTITY_ID,equal(entityId));
|
||||
if (userId>0) query.where(USER_ID,equal(userId));
|
||||
query.execute(db);
|
||||
} catch (SQLException e){
|
||||
throw failedToDropObject(Translatable.t("{module}.{id}", MODULE,module, ID,entityId)).causedBy(e);
|
||||
}
|
||||
@@ -327,6 +336,19 @@ CREATE TABLE IF NOT EXISTS {0} (
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> search(String key, UmbrellaUser user) {
|
||||
try {
|
||||
var tags = new HashSet<String>();
|
||||
var rs = select("DISTINCT tag").from(TABLE_TAGS).where(USER_ID,equal(user.id())).where(Field.TAG,like("%"+key+"%")).exec(db);
|
||||
while (rs.next()) tags.add(rs.getString(1));
|
||||
rs.close();
|
||||
return tags;
|
||||
} catch (SQLException s){
|
||||
throw failedToLoadObject(Text.TAGS);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateId(String module, Object oldId, Object newId) {
|
||||
try {
|
||||
|
||||
@@ -3,6 +3,7 @@ package de.srsoftware.umbrella.tags;
|
||||
|
||||
|
||||
import de.srsoftware.tools.Tuple;
|
||||
import de.srsoftware.umbrella.core.model.UmbrellaUser;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -13,6 +14,8 @@ public interface TagDB {
|
||||
|
||||
void deleteEntity(String module, long entityId);
|
||||
|
||||
void deleteEntity(String module, long entityId, long userId);
|
||||
|
||||
Map<String, List<Long>> getUses(String tag, long id);
|
||||
|
||||
Collection<Tuple<String, Long>> list(long userId);
|
||||
@@ -30,5 +33,7 @@ public interface TagDB {
|
||||
|
||||
void save(Collection<Long> userIds, String module, long entityId, Collection<String> tags);
|
||||
|
||||
Collection<String> search(String key, UmbrellaUser user);
|
||||
|
||||
void updateId(String module, Object oldId, Object newId);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ 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.USER_LIST;
|
||||
import static de.srsoftware.umbrella.core.constants.Path.SEARCH;
|
||||
import static de.srsoftware.umbrella.core.constants.Path.USES;
|
||||
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*;
|
||||
import static de.srsoftware.umbrella.tags.Constants.*;
|
||||
@@ -66,11 +67,13 @@ public class TagModule extends BaseHandler implements TagService {
|
||||
var user = userService().refreshSession(ex);
|
||||
if (user.isEmpty()) return unauthorized(ex);
|
||||
var module = path.pop();
|
||||
if (module == null) return getUserTags(ex, user.get());
|
||||
var head = path.pop();
|
||||
if (USES.equals(module)) return getTagUses(ex,head,user.get());
|
||||
long entityId = Long.parseLong(head);
|
||||
return sendContent(ex, getTags(module,entityId,user.get()));
|
||||
return switch (module){
|
||||
case SEARCH -> searchTags(ex,path.pop(),user.get());
|
||||
case USES -> getTagUses(ex,path.pop(),user.get());
|
||||
case null -> getUserTags(ex, user.get());
|
||||
default -> sendContent(ex, getTags(module,Long.parseLong(path.pop()),user.get()));
|
||||
};
|
||||
|
||||
} catch (NumberFormatException e){
|
||||
return sendContent(ex,HTTP_UNPROCESSABLE,"Entity id missing in path.");
|
||||
} catch (UmbrellaException e){
|
||||
@@ -134,6 +137,11 @@ public class TagModule extends BaseHandler implements TagService {
|
||||
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
|
||||
public void save(String module, long entityId, Collection<Long> userIds, Collection<String> tags) {
|
||||
tagDb.save(userIds,module,entityId,tags);
|
||||
@@ -145,6 +153,10 @@ public class TagModule extends BaseHandler implements TagService {
|
||||
return tag;
|
||||
}
|
||||
|
||||
private boolean searchTags(HttpExchange ex, String head, UmbrellaUser user) throws IOException {
|
||||
return sendContent(ex, tagDb.search(head, user));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateId(String module, Object oldId, Object newId) {
|
||||
tagDb.updateId(module,oldId,newId);
|
||||
|
||||
@@ -271,6 +271,12 @@ public class TaskModule extends BaseHandler implements TaskService {
|
||||
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) {
|
||||
var parent = taskDb.load(newParent);
|
||||
while (parent != null) {
|
||||
@@ -422,21 +428,23 @@ public class TaskModule extends BaseHandler implements TaskService {
|
||||
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 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 (parentTaskId == null) {
|
||||
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);
|
||||
loadMembers(projectTasks.values());
|
||||
var tags = tagService().getTags(TASK,projectTasks.keySet(),user);
|
||||
|
||||
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)));
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user