Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cf485055a6 | |||
| 968e5bfb95 | |||
| 669853352e | |||
| bdb3443240 | |||
| 43ebb241e8 | |||
| 1187956625 | |||
| fe0068f5ed | |||
| e92a4bedb9 | |||
| 11d14afb00 | |||
| 77e546bd4b | |||
| beb58bbc36 | |||
| f4adf2ca3c | |||
| dddba981c0 | |||
| b17275a623 | |||
| c1adce5a9e | |||
| a278f73c2e | |||
| 218d14090c | |||
| 6f2bbdf635 | |||
| 8cff93c469 | |||
| 1efed60696 | |||
| c6dac692de | |||
| 202d7deeb9 | |||
| fc2e484aec | |||
| bedb275fe3 | |||
| ff9b520449 | |||
| 2f4c939e93 | |||
| e596b0cdad | |||
| e50702d0b3 | |||
| f5cd51bc3b | |||
| c05f5110e4 | |||
| 2d4def4265 | |||
| 18c7965b2e | |||
| f6813d8a75 | |||
| c05f24cd7c | |||
| 5a1c8e1438 | |||
| 711addd75c | |||
| 78cae4644d | |||
| 851df52458 | |||
| bd2fb255d2 | |||
| 3b3803dafa | |||
| bad244ef16 | |||
| 3d31ac90a0 | |||
| 73751c1ea2 | |||
| a924f25f51 |
@@ -0,0 +1,76 @@
|
||||
name: Build Docker Image
|
||||
run-name: ${{ gitea.actor }} building ${{ gitea.ref_name }}
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
Docker-Build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build docker image
|
||||
run: docker build -t umbrella .
|
||||
|
||||
- name: Store tag date
|
||||
run: |
|
||||
TAG=$(date +%Y%m%d_%H%M)_${{ gitea.ref_name }}
|
||||
echo $TAG > /tmp/tag
|
||||
echo Using '"'$TAG'"' as tag.
|
||||
|
||||
- name: Tag image for upload
|
||||
run: |
|
||||
TAG=$(cat /tmp/tag)
|
||||
docker tag umbrella ${{ secrets.REGISTRY_PATH }}/umbrella:${{ gitea.ref_name }}
|
||||
docker tag umbrella ${{ secrets.REGISTRY_PATH }}/umbrella:$TAG
|
||||
|
||||
- name: Login to registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ${{ secrets.REGISTRY_PATH }}
|
||||
username: ${{ secrets.REGISTRY_USER }}
|
||||
password: ${{ secrets.REGISTRY_PASS }}
|
||||
|
||||
- name: Push to registry
|
||||
run: |
|
||||
TAG=$(cat /tmp/tag)
|
||||
docker push ${{ secrets.REGISTRY_PATH }}/umbrella:${{ gitea.ref_name }}
|
||||
docker push ${{ secrets.REGISTRY_PATH }}/umbrella:$TAG
|
||||
|
||||
- name: Restart vj.srsoftware.de
|
||||
if: github.ref == 'refs/heads/main'
|
||||
run: |
|
||||
curl -X POST -H "Authorization: Bearer ${{ secrets.MAKE_BEARER }}" -d vj_start https://make.srsoftware.de/launch
|
||||
|
||||
Clean-Registry:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get tag list
|
||||
run: |
|
||||
TAGS="$(curl -s -u "${{ secrets.REGISTRY_USER }}:${{ secrets.REGISTRY_PASS }}" https://${{ secrets.REGISTRY_PATH }}/v2/umbrella/tags/list | jq -r ".tags[]")"
|
||||
COUNT=$(echo "$TAGS" | wc -l)
|
||||
if [ $COUNT -gt 10 ]; then
|
||||
REMAIN=$((COUNT - 10))
|
||||
echo "$TAGS" | head -n $REMAIN > /tmp/old_tags
|
||||
else
|
||||
echo less than 10 tags, skipping cleanup
|
||||
echo "" > /tmp/old_tags
|
||||
fi
|
||||
|
||||
- name: Remove tags
|
||||
run: |
|
||||
cat /tmp/old_tags | while read TAG; do
|
||||
if [ -n "$TAG" ]; then
|
||||
DIGEST=$(curl -u "${{ secrets.REGISTRY_USER }}:${{ secrets.REGISTRY_PASS }}" -sS -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -o /dev/null -w '%header{Docker-Content-Digest}' https://${{ secrets.REGISTRY_PATH }}/v2/umbrella/manifests/$TAG)
|
||||
if [ -n "$DIGEST" ]; then
|
||||
echo about to delete $TAG
|
||||
curl -u "${{ secrets.REGISTRY_USER }}:${{ secrets.REGISTRY_PASS }}" -sS -X DELETE https://${{ secrets.REGISTRY_PATH }}/v2/umbrella/manifests/$DIGEST
|
||||
else
|
||||
echo failed to get digest for $TAG
|
||||
fi
|
||||
fi
|
||||
done
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
FROM alpine:3.22 AS svelte_build
|
||||
RUN apk add npm
|
||||
RUN adduser -Dh /home/svelte svelte
|
||||
ADD . /home/svelte/Umbrella
|
||||
RUN chown -R svelte /home/svelte/Umbrella
|
||||
USER svelte
|
||||
WORKDIR /home/svelte/Umbrella/frontend
|
||||
RUN npm install && npm run build
|
||||
|
||||
|
||||
FROM alpine AS java_build
|
||||
RUN apk add gradle fontconfig font-opensans openjdk21-jre
|
||||
ADD . /Umbrella
|
||||
WORKDIR /Umbrella
|
||||
COPY --from=svelte_build /home/svelte/Umbrella/frontend/dist web/src/main/resources/web
|
||||
RUN gradle --no-daemon build
|
||||
|
||||
|
||||
FROM alpine
|
||||
RUN apk add bash fontconfig font-opensans graphviz openjdk21-jre weasyprint
|
||||
RUN adduser -D umbrella
|
||||
COPY --from=java_build /Umbrella/backend/build/libs/backend.jar /home/umbrella/jar/
|
||||
RUN chown -R umbrella /home/umbrella
|
||||
ADD https://github.com/plantuml/plantuml/releases/download/v1.2025.10/plantuml-1.2025.10.jar /home/umbrella/plantuml.jar
|
||||
USER umbrella
|
||||
WORKDIR /home/umbrella
|
||||
RUN mkdir .config && ln -s /host/config.json .config/Umbrella.json
|
||||
EXPOSE 80
|
||||
CMD java -jar jar/backend.jar
|
||||
+6
-4
@@ -1,16 +1,18 @@
|
||||
default: devel
|
||||
|
||||
build: image
|
||||
docker run --name svelte-build \
|
||||
podman run --name svelte-build \
|
||||
--rm \
|
||||
--userns=keep-id:uid=$$(id -u),gid=$$(id -g) \
|
||||
-v ../frontend:/home/svelte/frontend \
|
||||
-ti svelte /opt/svelte-build
|
||||
image:
|
||||
docker build -t svelte .
|
||||
podman build --build-arg UID=$$(id -u) --build-arg GID=$$(id -g) -t svelte .
|
||||
|
||||
devel: image
|
||||
-docker rm -f svelte
|
||||
docker run --name svelte \
|
||||
-podman rm -f svelte
|
||||
podman run --name svelte \
|
||||
--userns=keep-id:uid=$$(id -u),gid=$$(id -g) \
|
||||
-v ../frontend:/home/svelte/frontend \
|
||||
-p 5173:5173 \
|
||||
-ti svelte /opt/svelte-init
|
||||
|
||||
@@ -12,6 +12,7 @@ application{
|
||||
|
||||
dependencies{
|
||||
implementation(project(":bookmark"));
|
||||
implementation(project(":bus"));
|
||||
implementation(project(":company"))
|
||||
implementation(project(":contact"))
|
||||
implementation(project(":core"))
|
||||
@@ -45,6 +46,7 @@ tasks.jar {
|
||||
from(dependencies)
|
||||
dependsOn(
|
||||
":bookmark:jar",
|
||||
":bus:jar",
|
||||
":company:jar",
|
||||
":contact:jar",
|
||||
":core:jar",
|
||||
|
||||
@@ -19,6 +19,7 @@ import de.srsoftware.umbrella.files.FileModule;
|
||||
import de.srsoftware.umbrella.legacy.*;
|
||||
import de.srsoftware.umbrella.markdown.MarkdownApi;
|
||||
import de.srsoftware.umbrella.message.MessageSystem;
|
||||
import de.srsoftware.umbrella.messagebus.MessageApi;
|
||||
import de.srsoftware.umbrella.notes.NoteModule;
|
||||
import de.srsoftware.umbrella.project.ProjectModule;
|
||||
import de.srsoftware.umbrella.stock.StockModule;
|
||||
@@ -63,6 +64,7 @@ public class Application {
|
||||
var server = HttpServer.create(new InetSocketAddress(port), 0);
|
||||
try {
|
||||
new Translations().bindPath("/api/translations").on(server);
|
||||
new MessageApi().bindPath("/api/bus").on(server);
|
||||
new MessageSystem(config);
|
||||
new UserModule(config).bindPath("/api/user").on(server);
|
||||
new TagModule(config).bindPath("/api/tags").on(server);
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ subprojects {
|
||||
implementation("de.srsoftware:configuration.api:1.0.2")
|
||||
implementation("de.srsoftware:tools.jdbc:2.0.7")
|
||||
implementation("de.srsoftware:tools.http:6.0.5")
|
||||
implementation("de.srsoftware:tools.mime:1.1.3")
|
||||
implementation("de.srsoftware:tools.mime:1.1.4")
|
||||
implementation("de.srsoftware:tools.logging:1.3.2")
|
||||
implementation("de.srsoftware:tools.optionals:1.0.0")
|
||||
implementation("de.srsoftware:tools.util:2.0.4")
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
description = "Umbrella : Messagebus"
|
||||
|
||||
dependencies{
|
||||
implementation(project(":core"))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.messagebus;
|
||||
|
||||
import de.srsoftware.umbrella.messagebus.events.Event;
|
||||
|
||||
public interface EventListener {
|
||||
void onEvent(Event event);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.messagebus;
|
||||
|
||||
import static de.srsoftware.umbrella.messagebus.MessageBus.messageBus;
|
||||
|
||||
import de.srsoftware.umbrella.messagebus.events.Event;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.LinkedList;
|
||||
|
||||
public class EventQueue extends LinkedList<Event> implements AutoCloseable, EventListener {
|
||||
|
||||
private final InetSocketAddress addr;
|
||||
|
||||
public EventQueue(InetSocketAddress addr){
|
||||
this.addr = addr;
|
||||
messageBus().register(this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void close() {
|
||||
messageBus().drop(this);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return addr.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEvent(Event event) {
|
||||
System.getLogger(addr.toString()).log(System.Logger.Level.INFO,"adding event to queue of {1}: {0}",event.eventType(),addr);
|
||||
add(event);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.messagebus;
|
||||
|
||||
import static de.srsoftware.umbrella.core.Constants.*;
|
||||
import static de.srsoftware.umbrella.core.ModuleRegistry.userService;
|
||||
import static java.lang.System.Logger.Level.*;
|
||||
import static java.lang.Thread.sleep;
|
||||
import static java.net.HttpURLConnection.HTTP_OK;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import de.srsoftware.tools.MimeType;
|
||||
import de.srsoftware.tools.Path;
|
||||
import de.srsoftware.tools.SessionToken;
|
||||
import de.srsoftware.umbrella.core.BaseHandler;
|
||||
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
|
||||
import de.srsoftware.umbrella.core.model.Token;
|
||||
import de.srsoftware.umbrella.messagebus.events.Event;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Optional;
|
||||
|
||||
public class MessageApi extends BaseHandler{
|
||||
|
||||
private static final System.Logger LOG = System.getLogger(MessageApi.class.getSimpleName());
|
||||
|
||||
@Override
|
||||
public boolean doGet(Path path, HttpExchange ex) throws IOException {
|
||||
addCors(ex);
|
||||
Optional<Token> token = SessionToken.from(ex).map(Token::of);
|
||||
var user = userService().loadUser(token);
|
||||
if (user.isEmpty()) return unauthorized(ex); // TODO
|
||||
|
||||
var headers = ex.getResponseHeaders();
|
||||
var addr = ex.getRemoteAddress();
|
||||
|
||||
headers.add(CONTENT_TYPE, MimeType.MIME_EVENT_STREAM);
|
||||
headers.add(CACHE_CONTROL,NO_CACHE);
|
||||
headers.add(CONNECTION,KEEP_ALIVE);
|
||||
headers.add(CONTENT_ENCODING,NONE);
|
||||
ex.sendResponseHeaders(HTTP_OK,0);
|
||||
try (var os = ex.getResponseBody(); var stream = new PrintWriter(os); var eventQueue = new EventQueue(addr)){
|
||||
LOG.log(INFO,"{0} opened event stream.",addr);
|
||||
var counter = 0;
|
||||
while (!stream.checkError()){
|
||||
sleep(100);
|
||||
if (eventQueue.isEmpty()){
|
||||
if (++counter > 300) counter = sendBeacon(addr,stream);
|
||||
} else {
|
||||
var event = eventQueue.removeFirst();
|
||||
//if (event.isIntendedFor(user.get())) {
|
||||
LOG.log(DEBUG, "sending event to {0}", addr);
|
||||
sendEvent(stream, event);
|
||||
counter = 0;
|
||||
//}
|
||||
}
|
||||
}
|
||||
LOG.log(INFO,"{0} disconnected from event stream.",addr);
|
||||
return true;
|
||||
} catch (InterruptedException e) {
|
||||
throw new UmbrellaException("EventStream broken").causedBy(e);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendEvent(PrintWriter out, Event event) {
|
||||
if (event == null) return;
|
||||
out.print("event: ");
|
||||
out.println(event.eventType());
|
||||
out.print("data: ");
|
||||
out.println(event.json());
|
||||
out.println();
|
||||
out.println(); // terminate message with empty line
|
||||
out.flush();
|
||||
}
|
||||
|
||||
private int sendBeacon(InetSocketAddress addr, PrintWriter stream) {
|
||||
LOG.log(DEBUG,"sending keep-alive to {0}",addr);
|
||||
stream.print(": keep-alive\n\n");
|
||||
stream.flush();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.messagebus;
|
||||
|
||||
import de.srsoftware.umbrella.messagebus.events.Event;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class MessageBus {
|
||||
private static MessageBus SINGLETON = new MessageBus();
|
||||
private Set<EventListener> listeners = new HashSet<>();
|
||||
|
||||
private MessageBus(){}
|
||||
|
||||
public void dispatch(Event event){
|
||||
new Thread(() -> { // TODO: use thread pool
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
listeners.parallelStream().forEach(l -> l.onEvent(event));
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
public void drop(EventListener listener){
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
public static MessageBus messageBus(){
|
||||
return SINGLETON;
|
||||
}
|
||||
|
||||
public void register(EventListener listener){
|
||||
listeners.add(listener);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.messagebus.events;
|
||||
|
||||
import static de.srsoftware.umbrella.core.Constants.USER;
|
||||
|
||||
import de.srsoftware.tools.Mappable;
|
||||
import de.srsoftware.umbrella.core.model.UmbrellaUser;
|
||||
import java.util.Map;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public abstract class Event<Payload extends Mappable> {
|
||||
|
||||
public enum EventType {
|
||||
CREATE,
|
||||
UPDATE,
|
||||
DELETE;
|
||||
|
||||
}
|
||||
private UmbrellaUser initiator;
|
||||
|
||||
private String realm;
|
||||
private Payload payload;
|
||||
private EventType eventType;
|
||||
public Event(UmbrellaUser initiator, String realm, Payload payload, EventType type){
|
||||
this.initiator = initiator;
|
||||
this.realm = realm;
|
||||
this.payload = payload;
|
||||
this.eventType = type;
|
||||
}
|
||||
|
||||
public String eventType(){
|
||||
return eventType.toString();
|
||||
}
|
||||
|
||||
public abstract boolean isIntendedFor(UmbrellaUser user);
|
||||
|
||||
public String json(){
|
||||
Class<?> clazz = payload.getClass();
|
||||
{ // get the highest superclass that is not object
|
||||
Class<?> parent = clazz.getSuperclass();
|
||||
|
||||
while (parent != null && parent != Object.class) {
|
||||
clazz = parent;
|
||||
parent = clazz.getSuperclass();
|
||||
}
|
||||
}
|
||||
var map = Map.of(USER,initiator.toMap(),clazz.getSimpleName().toLowerCase(),payload.toMap());
|
||||
return new JSONObject(map).toString();
|
||||
}
|
||||
|
||||
public Payload payload(){
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.messagebus.events;
|
||||
|
||||
import static de.srsoftware.umbrella.core.Constants.PROJECT;
|
||||
|
||||
import de.srsoftware.umbrella.core.model.Project;
|
||||
import de.srsoftware.umbrella.core.model.UmbrellaUser;
|
||||
|
||||
|
||||
public class ProjectEvent extends Event<Project>{
|
||||
public ProjectEvent(UmbrellaUser initiator, Project project, EventType type){
|
||||
super(initiator, PROJECT, project, type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isIntendedFor(UmbrellaUser user) {
|
||||
for (var member : payload().members().values()){
|
||||
if (member.user().equals(user)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.messagebus.events;
|
||||
|
||||
import static de.srsoftware.umbrella.core.Constants.TASK;
|
||||
|
||||
import de.srsoftware.umbrella.core.model.Task;
|
||||
import de.srsoftware.umbrella.core.model.UmbrellaUser;
|
||||
|
||||
|
||||
public class TaskEvent extends Event<Task>{
|
||||
public TaskEvent(UmbrellaUser initiator, Task task, EventType type){
|
||||
super(initiator, TASK, task, type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isIntendedFor(UmbrellaUser user) {
|
||||
for (var member : payload().members().values()){
|
||||
if (member.user().equals(user)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -20,12 +20,15 @@ public class Constants {
|
||||
public static final String BODY = "body";
|
||||
public static final String BOOKMARK = "bookmark";
|
||||
|
||||
public static final String CACHE_CONTROL = "Cache-Control";
|
||||
public static final String CODE = "code";
|
||||
public static final String COMMENT = "comment";
|
||||
public static final String COMPANY = "company";
|
||||
public static final String COMPANY_ID = "company_id";
|
||||
public static final String CONNECTION = "Connection";
|
||||
public static final String CONTENT = "content";
|
||||
public static final String CONTENT_DISPOSITION = "Content-Disposition";
|
||||
public static final String CONTENT_ENCODING = "Content-Encoding";
|
||||
public static final String CONTENT_TYPE = "Content-Type";
|
||||
public static final String CUSTOMER_NUMBER_PREFIX = "customer_number_prefix";
|
||||
|
||||
@@ -67,7 +70,9 @@ public class Constants {
|
||||
|
||||
public static final String JSONARRAY = "json array";
|
||||
public static final String JSONOBJECT = "json object";
|
||||
public static final String KEY = "key";
|
||||
|
||||
public static final String KEEP_ALIVE = "keep-alive";
|
||||
public static final String KEY = "key";
|
||||
|
||||
public static final String LANGUAGE = "language";
|
||||
public static final String LAST_CUSTOMER_NUMBER = "last_customer_number";
|
||||
@@ -80,12 +85,14 @@ public class Constants {
|
||||
public static final String MESSAGES = "messages";
|
||||
public static final String MODULE = "module";
|
||||
|
||||
public static final String NAME = "name";
|
||||
public static final String NAME = "name";
|
||||
public static final String NEW_MEMBER = "new_member";
|
||||
public static final String MIME = "mime";
|
||||
public static final String NO_INDEX = "no_index";
|
||||
public static final String NOTE = "note";
|
||||
public static final String NUMBER = "number";
|
||||
public static final String MIME = "mime";
|
||||
public static final String NO_CACHE = "no-cache";
|
||||
public static final String NO_INDEX = "no_index";
|
||||
public static final String NONE = "none";
|
||||
public static final String NOTE = "note";
|
||||
public static final String NUMBER = "number";
|
||||
|
||||
public static final String OFFSET = "offset";
|
||||
public static final String OPTIONAL = "optional";
|
||||
@@ -123,13 +130,14 @@ public class Constants {
|
||||
public static final String SUBJECT = "subject";
|
||||
|
||||
public static final String TABLE_SETTINGS = "settings";
|
||||
public static final String TAGS = "tags";
|
||||
public static final String TAGS = "tags";
|
||||
public static final String TAG_COLORS = "tag_colors";
|
||||
public static final String TASK_IDS = "task_ids";
|
||||
public static final String TAX = "tax";
|
||||
public static final String TAX_RATE = "tax_rate";
|
||||
public static final String TEMPLATE = "template";
|
||||
public static final String TEXT = "text";
|
||||
public static final String TASK = "task";
|
||||
public static final String TASK_IDS = "task_ids";
|
||||
public static final String TAX = "tax";
|
||||
public static final String TAX_RATE = "tax_rate";
|
||||
public static final String TEMPLATE = "template";
|
||||
public static final String TEXT = "text";
|
||||
public static final String THOUSANDS_SEPARATOR = "thousands_separator";
|
||||
public static final String THEME = "theme";
|
||||
public static final String TITLE = "title";
|
||||
|
||||
@@ -207,7 +207,7 @@ public final class Document implements Mappable {
|
||||
case SENDER: if (json.get(key) instanceof JSONObject nested) sender.patch(nested); break;
|
||||
case STATE: state = State.of(json.getInt(key)).orElseThrow(() -> new UmbrellaException(HTTP_UNPROCESSABLE,"Invalid state")); break;
|
||||
case POS: if (json.get(key) instanceof JSONObject nested) positions.patch(nested); break;
|
||||
case TEMPLATE_ID: if (json.get(key) instanceof String templateId) template = templateId; break;
|
||||
case TEMPLATE: if (json.get(key) instanceof String templateId) template = templateId; break;
|
||||
default: key = null;
|
||||
}
|
||||
if (key != null) dirtyFields.add(key);
|
||||
|
||||
@@ -18,7 +18,7 @@ public class WikiPage implements Mappable {
|
||||
private final long id;
|
||||
private String title;
|
||||
private int version;
|
||||
private final List<Integer> versions = new ArrayList<>();
|
||||
private final Set<Integer> versions = new TreeSet<>();
|
||||
private final Map<Long,Member> members = new HashMap<>();
|
||||
private String content;
|
||||
private Set<String> dirtyFields = new HashSet<>();
|
||||
@@ -157,7 +157,7 @@ public class WikiPage implements Mappable {
|
||||
return version;
|
||||
}
|
||||
|
||||
public List<Integer> versions(){
|
||||
public Set<Integer> versions(){
|
||||
return versions;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -9,6 +9,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
<script type="module" src="/src/main.js">
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Generated
+675
@@ -27,6 +27,278 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
|
||||
"integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz",
|
||||
"integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz",
|
||||
"integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz",
|
||||
"integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz",
|
||||
"integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz",
|
||||
"integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz",
|
||||
"integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz",
|
||||
"integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz",
|
||||
"integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.25.5",
|
||||
"cpu": [
|
||||
@@ -42,6 +314,142 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz",
|
||||
"integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.8",
|
||||
"license": "MIT",
|
||||
@@ -80,6 +488,216 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.1.tgz",
|
||||
"integrity": "sha512-JAcBr1+fgqx20m7Fwe1DxPUl/hPkee6jA6Pl7n1v2EFiktAHenTaXl5aIFjUIEsfn9w3HE4gK1lEgNGMzBDs1w==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.44.1.tgz",
|
||||
"integrity": "sha512-RurZetXqTu4p+G0ChbnkwBuAtwAbIwJkycw1n6GvlGlBuS4u5qlr5opix8cBAYFJgaY05TWtM+LaoFggUmbZEQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.1.tgz",
|
||||
"integrity": "sha512-fM/xPesi7g2M7chk37LOnmnSTHLG/v2ggWqKj3CCA1rMA4mm5KVBT1fNoswbo1JhPuNNZrVwpTvlCVggv8A2zg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.1.tgz",
|
||||
"integrity": "sha512-gDnWk57urJrkrHQ2WVx9TSVTH7lSlU7E3AFqiko+bgjlh78aJ88/3nycMax52VIVjIm3ObXnDL2H00e/xzoipw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.44.1.tgz",
|
||||
"integrity": "sha512-wnFQmJ/zPThM5zEGcnDcCJeYJgtSLjh1d//WuHzhf6zT3Md1BvvhJnWoy+HECKu2bMxaIcfWiu3bJgx6z4g2XA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.44.1.tgz",
|
||||
"integrity": "sha512-uBmIxoJ4493YATvU2c0upGz87f99e3wop7TJgOA/bXMFd2SvKCI7xkxY/5k50bv7J6dw1SXT4MQBQSLn8Bb/Uw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.44.1.tgz",
|
||||
"integrity": "sha512-n0edDmSHlXFhrlmTK7XBuwKlG5MbS7yleS1cQ9nn4kIeW+dJH+ExqNgQ0RrFRew8Y+0V/x6C5IjsHrJmiHtkxQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.44.1.tgz",
|
||||
"integrity": "sha512-8WVUPy3FtAsKSpyk21kV52HCxB+me6YkbkFHATzC2Yd3yuqHwy2lbFL4alJOLXKljoRw08Zk8/xEj89cLQ/4Nw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.1.tgz",
|
||||
"integrity": "sha512-yuktAOaeOgorWDeFJggjuCkMGeITfqvPgkIXhDqsfKX8J3jGyxdDZgBV/2kj/2DyPaLiX6bPdjJDTu9RB8lUPQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.1.tgz",
|
||||
"integrity": "sha512-W+GBM4ifET1Plw8pdVaecwUgxmiH23CfAUj32u8knq0JPFyK4weRy6H7ooxYFD19YxBulL0Ktsflg5XS7+7u9g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
|
||||
"version": "4.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.44.1.tgz",
|
||||
"integrity": "sha512-1zqnUEMWp9WrGVuVak6jWTl4fEtrVKfZY7CvcBmUUpxAJ7WcSowPSAWIKa/0o5mBL/Ij50SIf9tuirGx63Ovew==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
||||
"version": "4.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.44.1.tgz",
|
||||
"integrity": "sha512-Rl3JKaRu0LHIx7ExBAAnf0JcOQetQffaw34T8vLlg9b1IhzcBgaIdnvEbbsZq9uZp3uAH+JkHd20Nwn0h9zPjA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.44.1.tgz",
|
||||
"integrity": "sha512-j5akelU3snyL6K3N/iX7otLBIl347fGwmd95U5gS/7z6T4ftK288jKq3A5lcFKcx7wwzb5rgNvAg3ZbV4BqUSw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.44.1.tgz",
|
||||
"integrity": "sha512-ppn5llVGgrZw7yxbIm8TTvtj1EoPgYUAbfw0uDjIOzzoqlZlZrLJ/KuiE7uf5EpTpCTrNt1EdtzF0naMm0wGYg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.44.1.tgz",
|
||||
"integrity": "sha512-Hu6hEdix0oxtUma99jSP7xbvjkUM/ycke/AQQ4EC5g7jNRLLIwjcNwaUy95ZKBJJwg1ZowsclNnjYqzN4zwkAw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.44.1",
|
||||
"cpu": [
|
||||
@@ -104,6 +722,48 @@
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.1.tgz",
|
||||
"integrity": "sha512-NtSJVKcXwcqozOl+FwI41OH3OApDyLk3kqTJgx8+gp6On9ZEt5mYhIsKNPGuaZr3p9T6NWPKGU/03Vw4CNU9qg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.44.1.tgz",
|
||||
"integrity": "sha512-JYA3qvCOLXSsnTR3oiyGws1Dm0YTuxAAeaYGVlGpUsHqloPcFjPg+X0Fj2qODGLNwQOAcCiQmHub/V007kiH5A==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.1.tgz",
|
||||
"integrity": "sha512-J8o22LuF0kTe7m+8PvW9wk3/bRq5+mRo5Dqo6+vXb7otCm3TPhYOJqOaQtGU9YMWQSL3krMnoOxMr0+9E6F3Ug==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@sveltejs/acorn-typescript": {
|
||||
"version": "1.0.5",
|
||||
"license": "MIT",
|
||||
@@ -269,6 +929,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-reference": {
|
||||
"version": "3.0.3",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
<span class="error">{@html messages.error}</span>
|
||||
{/if}
|
||||
{#if messages.warning}
|
||||
<span class="error">{@html messages.warning}</span>
|
||||
<span class="warn">{@html messages.warning}</span>
|
||||
{/if}
|
||||
<Route path="/" component={User} />
|
||||
<Route path="/bookmark" component={Bookmarks} />
|
||||
@@ -130,7 +130,7 @@
|
||||
<span class="error">{@html messages.error}</span>
|
||||
{/if}
|
||||
{#if messages.warning}
|
||||
<span class="error">{@html messages.warning}</span>
|
||||
<span class="warn">{@html messages.warning}</span>
|
||||
{/if}
|
||||
<Route path="/user/reset/pw" component={ResetPw} />
|
||||
<Route path="/oidc_callback" component={Callback} />
|
||||
|
||||
@@ -116,6 +116,6 @@
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<svelte:element this={type} {onclick} {oncontextmenu} class={{editable}} title={t('right_click_to_edit')} >{@html target(editValue.rendered)}</svelte:element>
|
||||
<svelte:element this={type} {onclick} {oncontextmenu} class={{editable}} title={t('right_click_to_edit')} >{@html target(value.rendered)}</svelte:element>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -2,13 +2,14 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { useTinyRouter } from 'svelte-tiny-router';
|
||||
import { api } from '../../urls.svelte';
|
||||
import { error, yikes } from '../../warn.svelte';
|
||||
import { error, warn, yikes } from '../../warn.svelte';
|
||||
import { t } from '../../translations.svelte';
|
||||
import { user } from '../../user.svelte';
|
||||
|
||||
const image_extensions = ['jpg','jpeg','gif','png','svg','webp'];
|
||||
const router = useTinyRouter();
|
||||
let children = $state({});
|
||||
|
||||
let new_dir = $state(null);
|
||||
let files = $state();
|
||||
let parent = $state(false);
|
||||
@@ -17,23 +18,6 @@
|
||||
let delete_allowed = $state(false);
|
||||
let available = $derived.by(isAvailable);
|
||||
|
||||
function isAvailable(){
|
||||
if (!new_dir) return false;
|
||||
if (children){
|
||||
if (children.dirs) {
|
||||
for (let key of Object.values(children.dirs)){
|
||||
if (key == new_dir) return false;
|
||||
}
|
||||
}
|
||||
if (children.files) {
|
||||
for (let key of Object.values(children.files)){
|
||||
if (key == new_dir) return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function create_dir(ev){
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
@@ -74,13 +58,30 @@
|
||||
|
||||
async function handleDirectory(res){
|
||||
let json = await res.json();
|
||||
children.dirs = json.dirs ? json.dirs : {};
|
||||
children.files = json.files ? json.files : {};
|
||||
children.dirs = json.dirs ? val_sort(json.dirs) : {};
|
||||
children.files = json.files ? val_sort(json.files) : {};
|
||||
children.title = json.title ? json.title : path;
|
||||
delete_allowed = json.delete;
|
||||
yikes();
|
||||
}
|
||||
|
||||
function isAvailable(){
|
||||
if (!new_dir) return false;
|
||||
if (children){
|
||||
if (children.dirs) {
|
||||
for (let key of Object.values(children.dirs)){
|
||||
if (key == new_dir) return false;
|
||||
}
|
||||
}
|
||||
if (children.files) {
|
||||
for (let key of Object.values(children.files)){
|
||||
if (key == new_dir) return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function is_image(file){
|
||||
let parts = file.toLowerCase().split('.');
|
||||
let ext = parts.pop();
|
||||
@@ -90,12 +91,15 @@
|
||||
async function loadChildren(p){
|
||||
p = p.substring(6);
|
||||
if (p == '') p = '/';
|
||||
children = { dirs : {}, files : {}, title : p};
|
||||
children = { dirs : [], files : [], title : p};
|
||||
path = p;
|
||||
if (p == '/'){
|
||||
children.dirs[`/user/${user.id}`] = t('my_files');
|
||||
children.dirs['/project'] = t('projects')
|
||||
children.dirs['/company'] = t('companies');
|
||||
children.dirs = [
|
||||
{ path : `/user/${user.id}`, name : t('my files') },
|
||||
{ path : '/project', name : t('projects')},
|
||||
{ path : '/company', name : t('companies')},
|
||||
]
|
||||
|
||||
parent = false;
|
||||
form = false;
|
||||
} else {
|
||||
@@ -114,8 +118,12 @@
|
||||
|
||||
function markdown(file){
|
||||
let parts = file.split('/');
|
||||
let md = ``;
|
||||
let path = `/api/files${file}`;
|
||||
path = encodeURI(path);
|
||||
let md = ``;
|
||||
navigator.clipboard.writeText(md);
|
||||
warn(t('Markdown has been copied to clipboard!'));
|
||||
setTimeout(yikes, 2500);
|
||||
}
|
||||
|
||||
function onclick(ev){
|
||||
@@ -153,6 +161,12 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
function val_sort(map){
|
||||
return Object.entries(map)
|
||||
.map(item => ({name:item[1],path:item[0]}))
|
||||
.sort((a,b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
onMount(() => loadChildren(window.location.pathname));
|
||||
</script>
|
||||
|
||||
@@ -166,12 +180,12 @@
|
||||
</li>
|
||||
{/if}
|
||||
{#if children?.dirs}
|
||||
{#each Object.entries(children.dirs) as [k,v]}
|
||||
{#each children.dirs as dir}
|
||||
<li class="dir">
|
||||
<span class="symbol"></span>
|
||||
<a href={'/files'+k} {onclick}>{v}</a>
|
||||
<a href={'/files'+dir.path} {onclick}>{dir.name}</a>
|
||||
{#if delete_allowed}
|
||||
<button class="symbol" onclick={e => dropDir(`/api/files${k}`,v)}></button>
|
||||
<button class="symbol" onclick={e => dropDir(`/api/files${dir.path}`,dir.name)}></button>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
@@ -186,15 +200,15 @@
|
||||
</li>
|
||||
{/if}
|
||||
{#if children.files}
|
||||
{#each Object.entries(children.files) as [k,v]}
|
||||
{#each children.files as file}
|
||||
<li class="file">
|
||||
<span class="symbol"></span>
|
||||
<a href={`/api/files${k}`} target="_blank">{v}</a>
|
||||
{#if is_image(k)}
|
||||
<button class="symbol" title={'markdown_code'} onclick={e => markdown(k)}></button>
|
||||
<a href={`/api/files${file.path}`} target="_blank">{file.name}</a>
|
||||
{#if is_image(file.path)}
|
||||
<button class="symbol" title={'markdown_code'} onclick={e => markdown(file.path)}></button>
|
||||
{/if}
|
||||
{#if delete_allowed}
|
||||
<button class="symbol" title={t('delete_object',{'object':t('file')})} onclick={e => dropFile(`/api/files${k}`,v)}></button>
|
||||
<button class="symbol" title={t('delete_object',{'object':t('file')})} onclick={e => dropFile(`/api/files${file.path}`,file.name)}></button>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { useTinyRouter } from 'svelte-tiny-router';
|
||||
|
||||
import { api, target } from '../../urls.svelte.js';
|
||||
import { error, yikes } from '../../warn.svelte';
|
||||
import { api, 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';
|
||||
|
||||
@@ -11,10 +11,12 @@
|
||||
import LineEditor from '../../Components/LineEditor.svelte';
|
||||
import MarkdownEditor from '../../Components/MarkdownEditor.svelte';
|
||||
|
||||
let { id } = $props();
|
||||
let descr = $state(false);
|
||||
let filter_input = $state('');
|
||||
let router = useTinyRouter();
|
||||
let eventSource = null;
|
||||
let connectionStatus = 'disconnected';
|
||||
let { id } = $props();
|
||||
let descr = $state(false);
|
||||
let filter_input = $state('');
|
||||
let router = useTinyRouter();
|
||||
if (router.hasQueryParam('filter')) filter_input = router.getQueryParam('filter');
|
||||
let dragged = null;
|
||||
let highlight = $state({});
|
||||
@@ -23,34 +25,11 @@
|
||||
let tasks = $state({});
|
||||
let users = [];
|
||||
let columns = $derived(project.allowed_states?Object.keys(project.allowed_states).length+1:1);
|
||||
let info = $state(null);
|
||||
|
||||
let stateList = {};
|
||||
$effect(() => updateUrl(filter_input));
|
||||
|
||||
async function do_archive(ex){
|
||||
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})
|
||||
});
|
||||
delete highlight.archive;
|
||||
if (resp.ok){
|
||||
yikes();
|
||||
delete tasks[task.assignee][task.status][task.id]
|
||||
} else {
|
||||
error(resp);
|
||||
}
|
||||
}
|
||||
|
||||
function updateUrl(){
|
||||
let url = window.location.origin + window.location.pathname;
|
||||
if (filter_input) url += '?filter=' + encodeURI(filter_input);
|
||||
window.history.replaceState(window.history.state, '', url);
|
||||
}
|
||||
|
||||
function byName(a,b) {
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
@@ -85,6 +64,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function do_archive(ex){
|
||||
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})
|
||||
});
|
||||
delete highlight.archive;
|
||||
if (resp.ok){
|
||||
yikes();
|
||||
delete tasks[task.assignee][task.status][task.id]
|
||||
} else {
|
||||
error(resp);
|
||||
}
|
||||
}
|
||||
|
||||
async function drop(user_id,state){
|
||||
let task = dragged;
|
||||
dragged = null;
|
||||
@@ -100,20 +97,64 @@
|
||||
body : JSON.stringify(patch)
|
||||
});
|
||||
if (resp.ok){
|
||||
delete tasks[task.assignee][task.status][task.id]
|
||||
if (!tasks[user_id]) tasks[user_id] = {}
|
||||
if (!tasks[user_id][state]) tasks[user_id][state] = {}
|
||||
tasks[user_id][state][task.id] = task;
|
||||
task.assignee = user_id;
|
||||
task.status = state;
|
||||
yikes();
|
||||
} else {
|
||||
error(resp);
|
||||
}
|
||||
}
|
||||
|
||||
function handleCreateEvent(evt){
|
||||
handleEvent(evt,'create');
|
||||
}
|
||||
|
||||
function handleEvent(evt,method){
|
||||
let json = JSON.parse(evt.data);
|
||||
if (json.task && json.user){
|
||||
// drop from kanban
|
||||
for (let uid in tasks){
|
||||
if (!uid) continue;
|
||||
for (let state in tasks[uid]) delete tasks[uid][state][json.task.id];
|
||||
}
|
||||
|
||||
// (re) add to kanban
|
||||
if (method != 'delete') processTask(json.task);
|
||||
|
||||
// show notification
|
||||
if (json.user.id != user.id) {
|
||||
let term = "user_updated_entity";
|
||||
if (method == 'create') term = "user_created_entity";
|
||||
if (method == 'delete') term = "user_deleted_entity";
|
||||
info = t(term,{user:json.user.name,entity:json.task.name});
|
||||
setTimeout(() => { info = null; },2500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleDeleteEvent(evt){
|
||||
handleEvent(evt,'delete');
|
||||
}
|
||||
|
||||
function handleUpdateEvent(evt){
|
||||
handleEvent(evt,'update');
|
||||
}
|
||||
|
||||
function hover(ev,user_id,state){
|
||||
ev.preventDefault();
|
||||
highlight = {user:user_id,state:state};
|
||||
}
|
||||
|
||||
function hover_archive(ev){
|
||||
ev.preventDefault();
|
||||
highlight.archive = true;
|
||||
}
|
||||
|
||||
function is_custom(state){
|
||||
return [10,20,40,60,100].includes(state);
|
||||
}
|
||||
|
||||
async function load(){
|
||||
try {
|
||||
eventSource = eventStream(handleCreateEvent,handleUpdateEvent,handleDeleteEvent);
|
||||
await loadProject();
|
||||
loadTasks({project_id:+id,parent_task_id:0});
|
||||
} catch (ignored) {}
|
||||
@@ -155,55 +196,46 @@
|
||||
var json = await resp.json();
|
||||
for (var task_id of Object.keys(json)) {
|
||||
let task = json[task_id];
|
||||
if (task.no_index) continue;
|
||||
|
||||
let state = +task.status;
|
||||
let owner = null;
|
||||
let assignee = null;
|
||||
|
||||
for (var user_id of Object.keys(task.members)){
|
||||
var member = task.members[user_id];
|
||||
if (member.permission.name == 'OWNER') owner = +user_id;
|
||||
if (member.permission.name == 'ASSIGNEE') assignee = +user_id;
|
||||
}
|
||||
if (!assignee) assignee = owner;
|
||||
task.assignee = assignee;
|
||||
if (!tasks[assignee]) tasks[assignee] = {};
|
||||
if (!tasks[assignee][state]) tasks[assignee][state] = {};
|
||||
tasks[assignee][state][task_id] = task;
|
||||
processTask(task);
|
||||
}
|
||||
} else {
|
||||
error(resp);
|
||||
}
|
||||
}
|
||||
|
||||
function hover(ev,user_id,state){
|
||||
ev.preventDefault();
|
||||
highlight = {user:user_id,state:state};
|
||||
}
|
||||
|
||||
function hover_archive(ev){
|
||||
ev.preventDefault();
|
||||
highlight.archive = true;
|
||||
}
|
||||
|
||||
function is_custom(state){
|
||||
return [10,20,40,60,100].includes(state);
|
||||
}
|
||||
|
||||
function openTask(task_id){
|
||||
window.open(`/task/${task_id}/view`, '_blank').focus();
|
||||
}
|
||||
|
||||
function processTask(task){
|
||||
if (task.no_index) return;
|
||||
|
||||
let state = +task.status;
|
||||
let owner = null;
|
||||
let assignee = null;
|
||||
|
||||
for (var user_id of Object.keys(task.members)){
|
||||
var member = task.members[user_id];
|
||||
if (member.permission.name == 'OWNER') owner = +user_id;
|
||||
if (member.permission.name == 'ASSIGNEE') assignee = +user_id;
|
||||
}
|
||||
if (!assignee) assignee = owner;
|
||||
task.assignee = assignee;
|
||||
if (!tasks[assignee]) tasks[assignee] = {};
|
||||
if (!tasks[assignee][state]) tasks[assignee][state] = {};
|
||||
tasks[assignee][state][task.id] = task;
|
||||
}
|
||||
|
||||
|
||||
async function save_bookmark(){
|
||||
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}`,
|
||||
share: user_ids
|
||||
url : location.href,
|
||||
tags : ['Kanban', project.name, filter_input],
|
||||
comment : `${project.name}: ${filter_input}`,
|
||||
share : user_ids
|
||||
}
|
||||
const url = api('bookmark');
|
||||
const url = api('bookmark');
|
||||
const resp = await fetch(url,{
|
||||
credentials : 'include',
|
||||
method : 'POST',
|
||||
@@ -217,6 +249,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function updateUrl(){
|
||||
let url = window.location.origin + window.location.pathname;
|
||||
if (filter_input) url += '?filter=' + encodeURI(filter_input);
|
||||
window.history.replaceState(window.history.state, '', url);
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
if (eventSource) eventSource.close();
|
||||
});
|
||||
onMount(load);
|
||||
</script>
|
||||
|
||||
@@ -227,7 +269,9 @@
|
||||
{#if project}
|
||||
<h1 onclick={ev => router.navigate(`/project/${project.id}/view`)}>{project.name}</h1>
|
||||
{/if}
|
||||
|
||||
{#if info}
|
||||
<div class="info">{info}</div>
|
||||
{/if}
|
||||
{#if project}
|
||||
<fieldset class="kanban description {descr?'active':''}" onclick={e => descr = !descr}>
|
||||
<legend>{t('description')} – {t('expand_on_click')}</legend>
|
||||
|
||||
@@ -1,19 +1,26 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { useTinyRouter } from 'svelte-tiny-router';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { useTinyRouter } from 'svelte-tiny-router';
|
||||
|
||||
import { api } from '../../urls.svelte.js';
|
||||
import { error, yikes } from '../../warn.svelte';
|
||||
import { t } from '../../translations.svelte.js';
|
||||
import { api, eventStream } from '../../urls.svelte.js';
|
||||
import { error, yikes } from '../../warn.svelte';
|
||||
import { t } from '../../translations.svelte.js';
|
||||
|
||||
const router = useTinyRouter();
|
||||
let events = null;
|
||||
let projects = $state(null);
|
||||
let companies = $state(null);
|
||||
let showClosed = $state(router.query.closed == "show");
|
||||
|
||||
let sortedProjects = $derived.by(() => Object.values(projects).sort((a, b) => a.name.localeCompare(b.name)));
|
||||
|
||||
function handleUpdate(evt){
|
||||
let json = JSON.parse(evt.data);
|
||||
if (json.project) projects[json.project.id] = json.project;
|
||||
}
|
||||
|
||||
async function loadProjects(){
|
||||
events = eventStream(handleUpdate,handleUpdate,null);
|
||||
let url = api('company/list');
|
||||
let resp = await fetch(url,{credentials:'include'});
|
||||
if (resp.ok){
|
||||
@@ -66,6 +73,9 @@
|
||||
}
|
||||
|
||||
onMount(loadProjects);
|
||||
onDestroy(() => {
|
||||
if (events) events.close();
|
||||
})
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { useTinyRouter } from 'svelte-tiny-router';
|
||||
|
||||
import { api } from '../../urls.svelte';
|
||||
import { api, eventStream } from '../../urls.svelte';
|
||||
import { error, yikes } from '../../warn.svelte';
|
||||
import { t } from '../../translations.svelte';
|
||||
|
||||
@@ -14,7 +14,9 @@
|
||||
import Tags from '../tags/TagList.svelte';
|
||||
import TaskList from '../task/TaskList.svelte';
|
||||
|
||||
let eventSource = $state(null);
|
||||
let { id } = $props();
|
||||
let lastEvent = $state(null);
|
||||
let estimated_time = $state({sum:0});
|
||||
let project = $state(null);
|
||||
let router = useTinyRouter();
|
||||
@@ -80,10 +82,34 @@
|
||||
}
|
||||
}
|
||||
|
||||
function handleCreate(evt){
|
||||
let json = JSON.parse(evt.data);
|
||||
json.event = 'create';
|
||||
lastEvent = json;
|
||||
}
|
||||
|
||||
function handleDelete(evt){
|
||||
let json = JSON.parse(evt.data);
|
||||
json.event = 'delete';
|
||||
lastEvent = json;
|
||||
}
|
||||
|
||||
function handleUpdate(evt){
|
||||
let json = JSON.parse(evt.data);
|
||||
json.event = 'update';
|
||||
lastEvent = json;
|
||||
if (json.project && json.project.id == project.id) project = json.project;
|
||||
}
|
||||
|
||||
function kanban(){
|
||||
router.navigate(`/project/${id}/kanban`);
|
||||
}
|
||||
|
||||
function load(){
|
||||
eventSource = eventStream(handleCreate,handleUpdate,handleDelete);
|
||||
loadProject();
|
||||
}
|
||||
|
||||
async function loadProject(){
|
||||
const url = api(`project/${id}`);
|
||||
const resp = await fetch(url,{credentials:'include'});
|
||||
@@ -162,7 +188,20 @@
|
||||
window.open(`/time?project=${id}`, '_blank').focus();
|
||||
}
|
||||
|
||||
onMount(loadProject);
|
||||
$effect(() => {
|
||||
if (lastEvent && lastEvent.task) {
|
||||
if (lastEvent.event == 'delete' || lastEvent.task.parent_task_id){
|
||||
delete tasks[lastEvent.task.id];
|
||||
} else {
|
||||
tasks[lastEvent.task.id] = lastEvent.task;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onMount(load);
|
||||
onDestroy(() => {
|
||||
if (eventSource) eventSource.close();
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -265,7 +304,7 @@
|
||||
</div>
|
||||
<div class="tasks">
|
||||
{#if tasks}
|
||||
<TaskList {tasks} {estimated_time} states={project?.allowed_states} show_closed={show_closed || project.show_closed} />
|
||||
<TaskList {tasks} {estimated_time} {lastEvent} {eventSource} states={project?.allowed_states} show_closed={show_closed || project.show_closed} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
let {
|
||||
estimated_time,
|
||||
lastEvent,
|
||||
show_closed,
|
||||
siblings,
|
||||
states = {},
|
||||
@@ -53,7 +54,7 @@
|
||||
if (resp.ok){
|
||||
deleted = true;
|
||||
} else {
|
||||
error(resp);
|
||||
error(resp);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,8 +76,7 @@
|
||||
body : JSON.stringify({ parent_task_id : task.id})
|
||||
});
|
||||
if (resp.ok) {
|
||||
children[dragged.element.id]=dragged.element;
|
||||
if (dragged.siblings[dragged.element.id]) delete dragged.siblings[dragged.element.id];
|
||||
yikes();
|
||||
} else {
|
||||
error(resp);
|
||||
}
|
||||
@@ -141,6 +141,16 @@
|
||||
estimated_time.sum += task.estimated_time;
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (children && lastEvent && lastEvent.task) {
|
||||
if (lastEvent.event == 'delete' || lastEvent.task.parent_task_id != task.id){
|
||||
delete children[lastEvent.task.id];
|
||||
} else {
|
||||
children[lastEvent.task.id] = lastEvent.task;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onMount(loadChildren);
|
||||
</script>
|
||||
|
||||
@@ -170,7 +180,7 @@
|
||||
<button class="symbol" title={t('add_object',{object:t('subtask')})} onclick={addSubtask}></button>
|
||||
<button class="symbol" title={t('timetracking')} onclick={addTime}></button>
|
||||
{#if children}
|
||||
<TaskList {states} tasks={children} {estimated_time} {show_closed} />
|
||||
<TaskList {states} {lastEvent} tasks={children} {estimated_time} {show_closed} />
|
||||
{/if}
|
||||
</li>
|
||||
{/if}
|
||||
|
||||
@@ -2,14 +2,13 @@
|
||||
import { t } from '../../translations.svelte.js';
|
||||
import ListTask from './ListTask.svelte';
|
||||
|
||||
let { estimated_time, show_closed, states = {}, tasks } = $props();
|
||||
let { estimated_time, lastEvent, show_closed, states = {}, tasks } = $props();
|
||||
|
||||
let sortedTasks = $derived.by(() => Object.values(tasks).sort((a, b) => a.name.localeCompare(b.name)));
|
||||
|
||||
</script>
|
||||
|
||||
<ul>
|
||||
{#each sortedTasks as task}
|
||||
<ListTask {states} {task} siblings={tasks} {estimated_time} show_closed={show_closed || task.show_closed} />
|
||||
<ListTask {states} {task} {lastEvent} siblings={tasks} {estimated_time} show_closed={show_closed || task.show_closed} />
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { useTinyRouter } from 'svelte-tiny-router';
|
||||
|
||||
import { api } from '../../urls.svelte';
|
||||
import { api, eventStream } from '../../urls.svelte';
|
||||
import { error, yikes } from '../../warn.svelte';
|
||||
import { t } from '../../translations.svelte';
|
||||
import { timetrack } from '../../user.svelte.js';
|
||||
@@ -17,6 +17,7 @@
|
||||
import TagList from '../tags/TagList.svelte';
|
||||
import TaskList from './TaskList.svelte';
|
||||
|
||||
let eventSource = $state(null);
|
||||
let { id } = $props();
|
||||
let children = $state(null);
|
||||
let dummy = $derived(updateOn(id));
|
||||
@@ -27,7 +28,10 @@
|
||||
let show_closed = $state(false);
|
||||
let task = $state(null);
|
||||
|
||||
$effect(() => updateOn(id));
|
||||
$effect(() => {
|
||||
if (!eventSource) eventSource = eventStream(null,handleUpdateEvent,null);
|
||||
updateOn(id)
|
||||
});
|
||||
|
||||
function addChild(){
|
||||
router.navigate(`/task/${id}/add_subtask`);
|
||||
@@ -82,7 +86,15 @@
|
||||
router.navigate(`/project/${project.id}/view`)
|
||||
}
|
||||
|
||||
async function loadChildren(){
|
||||
function handleUpdateEvent(evt){
|
||||
let json = JSON.parse(evt.data);
|
||||
if (json.task) {
|
||||
if (json.task.id == id) updateOn(id);
|
||||
if (json.task.parent_task_id == id) children[json.task.id] = json.task;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadChildren(){
|
||||
const url = api('task/list');
|
||||
const data = {
|
||||
parent_task_id : +task.id,
|
||||
@@ -94,8 +106,8 @@
|
||||
body:JSON.stringify(data)
|
||||
});
|
||||
if (resp.ok){
|
||||
children = await resp.json();
|
||||
yikes();
|
||||
children = await resp.json();
|
||||
} else {
|
||||
error(resp);
|
||||
}
|
||||
@@ -117,8 +129,6 @@
|
||||
if (resp.ok){
|
||||
yikes();
|
||||
task = await resp.json();
|
||||
project = null;
|
||||
children = null;
|
||||
if (task.show_closed) show_closed = true;
|
||||
loadChildren();
|
||||
if (task.project_id) loadProject();
|
||||
@@ -167,13 +177,6 @@
|
||||
});
|
||||
if (resp.ok){
|
||||
yikes();
|
||||
let old_task = task;
|
||||
task = await resp.json();
|
||||
if (!task.parent_id){
|
||||
task.parent = null;
|
||||
} else {
|
||||
if (task.parent_id == old_task.parent_id) task.parent = old_task.parent;
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
error(resp);
|
||||
@@ -190,7 +193,7 @@
|
||||
}
|
||||
|
||||
function updateOn(id){
|
||||
task = null;
|
||||
//task = null;
|
||||
loadTask();
|
||||
}
|
||||
|
||||
@@ -199,6 +202,10 @@
|
||||
members[user_id] = permission.code;
|
||||
update({members:members});
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
if (eventSource) eventSource.close();
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -313,7 +320,7 @@
|
||||
</div>
|
||||
<div class="children">
|
||||
{#if children}
|
||||
<TaskList states={project?.allowed_states} tasks={children} {estimated_time} show_closed={task.show_closed} />
|
||||
<TaskList {eventSource} states={project?.allowed_states} tasks={children} {estimated_time} show_closed={task.show_closed} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
let projects = {};
|
||||
let project_filter = $state(null);
|
||||
if (router.hasQueryParam('project')) project_filter = router.getQueryParam('project');
|
||||
let sortedTimes = $derived.by(() => Object.values(times).map(time => ({
|
||||
let sortedTimes = $derived.by(() => Object.values(times).filter(match_prj_filter).map(time => ({
|
||||
...time,
|
||||
start: display(time.start_time),
|
||||
end: display(time.end_time),
|
||||
@@ -52,6 +52,7 @@
|
||||
}
|
||||
|
||||
function calcYearMap(){
|
||||
console.log('calcYearMap called');
|
||||
let result = {
|
||||
months : {},
|
||||
years : {}
|
||||
@@ -123,7 +124,7 @@
|
||||
function match_prj_filter(time){
|
||||
if (!project_filter) return true;
|
||||
for (var tid of time.task_ids){
|
||||
if (project_filter == tasks[tid].project_id) return true;
|
||||
if (tasks[tid] && project_filter == tasks[tid].project_id) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -258,7 +259,6 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each sortedTimes as time,line}
|
||||
{#if match_prj_filter(time)}
|
||||
<tr class={selected[time.id]?'selected':''}>
|
||||
{#if timeMap.years[line]}
|
||||
<td class="year" rowspan={timeMap.years[line]} onclick={e => toggleRange(time.start.substring(0,4))} title={time.start.substring(0,4)} >
|
||||
@@ -321,7 +321,6 @@
|
||||
</td>
|
||||
{/if}
|
||||
</tr>
|
||||
{/if}
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script>
|
||||
import Login from "../../Components/Login.svelte";
|
||||
|
||||
import { onMount } from 'svelte';
|
||||
import { useTinyRouter } from 'svelte-tiny-router';
|
||||
import { api } from '../../urls.svelte';
|
||||
@@ -45,3 +47,4 @@
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<Login />
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
let detail = $state(false);
|
||||
let { key, version } = $props();
|
||||
let page = $state(null);
|
||||
let page = $state({});
|
||||
let router = useTinyRouter();
|
||||
let members = $state({});
|
||||
let editable = $derived(page.members[user.id].permission.code<4);
|
||||
@@ -72,9 +72,9 @@
|
||||
|
||||
async function loadContent(res){
|
||||
if (res.ok){
|
||||
page = null;
|
||||
page = await res.json();
|
||||
page.versions.sort((a,b)=>b-a);
|
||||
let json = await res.json();
|
||||
json.versions.sort((a,b)=>b-a);
|
||||
page = { ...json };
|
||||
yikes();
|
||||
return true;
|
||||
} else {
|
||||
@@ -131,7 +131,7 @@
|
||||
|
||||
$effect(loadPage);
|
||||
</script>
|
||||
{#if page}
|
||||
{#if page && page.versions}
|
||||
<div class="wiki page">
|
||||
<div class="versions">
|
||||
<span class="version">{t('version')}</span>
|
||||
|
||||
@@ -15,6 +15,14 @@ export function drop(url){
|
||||
});
|
||||
}
|
||||
|
||||
export function eventStream(createHandler,updateHandler,deleteHandler){
|
||||
const es = new EventSource(api('bus'), {withCredentials: true});
|
||||
if (createHandler) es.addEventListener('CREATE', createHandler);
|
||||
if (updateHandler) es.addEventListener('UPDATE', updateHandler);
|
||||
if (deleteHandler) es.addEventListener('DELETE', deleteHandler);
|
||||
return es;
|
||||
}
|
||||
|
||||
export function patch(url,data){
|
||||
return fetch(url,{
|
||||
credentials : 'include',
|
||||
|
||||
@@ -15,5 +15,5 @@ export async function warn(msg){
|
||||
|
||||
export function yikes(){
|
||||
messages.error = null;
|
||||
messages.warn = null;
|
||||
messages.warning = null;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
description = "Umbrella : Projects"
|
||||
|
||||
dependencies{
|
||||
implementation(project(":bus"))
|
||||
implementation(project(":core"))
|
||||
}
|
||||
@@ -12,6 +12,9 @@ import static de.srsoftware.umbrella.core.model.Permission.*;
|
||||
import static de.srsoftware.umbrella.core.model.Permission.OWNER;
|
||||
import static de.srsoftware.umbrella.core.model.Status.OPEN;
|
||||
import static de.srsoftware.umbrella.core.model.Status.PREDEFINED;
|
||||
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 de.srsoftware.umbrella.project.Constants.CONFIG_DATABASE;
|
||||
import static java.lang.Boolean.TRUE;
|
||||
import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
|
||||
@@ -25,6 +28,7 @@ import de.srsoftware.umbrella.core.ModuleRegistry;
|
||||
import de.srsoftware.umbrella.core.api.ProjectService;
|
||||
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
|
||||
import de.srsoftware.umbrella.core.model.*;
|
||||
import de.srsoftware.umbrella.messagebus.events.ProjectEvent;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import org.json.JSONArray;
|
||||
@@ -211,7 +215,8 @@ public class ProjectModule extends BaseHandler implements ProjectService {
|
||||
if (json.has(MEMBERS) && json.get(MEMBERS) instanceof JSONObject memberJson) patchMembers(project,memberJson);
|
||||
if (json.has(NEW_MEMBER) && json.get(NEW_MEMBER) instanceof Number num) addMember(project,num.longValue());
|
||||
|
||||
projectDb.save(project.patch(json), user);
|
||||
project = projectDb.save(project.patch(json), user);
|
||||
messageBus().dispatch(new ProjectEvent(user,project, UPDATE));
|
||||
return sendContent(ex,project.toMap());
|
||||
}
|
||||
|
||||
@@ -252,7 +257,7 @@ public class ProjectModule extends BaseHandler implements ProjectService {
|
||||
var tagList = arr.toList().stream().filter(elem -> elem instanceof String).map(String.class::cast).toList();
|
||||
tagService().save(PROJECT,prj.id(),null,tagList);
|
||||
}
|
||||
|
||||
messageBus().dispatch(new ProjectEvent(user,prj, CREATE));
|
||||
return sendContent(ex,prj);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ rootProject.name = "Umbrella25"
|
||||
|
||||
include("backend")
|
||||
include("bookmark")
|
||||
include("bus")
|
||||
include("company")
|
||||
include("contact")
|
||||
include("core")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
description = "Umbrella : Tasks"
|
||||
|
||||
dependencies{
|
||||
implementation(project(":bus"))
|
||||
implementation(project(":core"))
|
||||
implementation(project(":project"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ public class Constants {
|
||||
public static final String TABLE_TASK_DEPENDENCIES = "task_dependencies";
|
||||
public static final String TABLE_TASKS = "tasks";
|
||||
public static final String TABLE_TASKS_USERS = "tasks_users";
|
||||
public static final String TASK = "task";
|
||||
public static final String TASKS = "tasks";
|
||||
public static final String TASK_ID = "task_id";
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.notFound;
|
||||
import static de.srsoftware.umbrella.core.model.Status.*;
|
||||
import static de.srsoftware.umbrella.project.Constants.*;
|
||||
import static de.srsoftware.umbrella.task.Constants.*;
|
||||
import static java.lang.System.Logger.Level.*;
|
||||
import static java.text.MessageFormat.format;
|
||||
|
||||
import de.srsoftware.tools.jdbc.Query;
|
||||
|
||||
@@ -11,6 +11,9 @@ import static de.srsoftware.umbrella.core.Util.mapValues;
|
||||
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*;
|
||||
import static de.srsoftware.umbrella.core.model.Permission.*;
|
||||
import static de.srsoftware.umbrella.core.model.Permission.OWNER;
|
||||
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 de.srsoftware.umbrella.project.Constants.PERMISSIONS;
|
||||
import static de.srsoftware.umbrella.task.Constants.*;
|
||||
import static java.lang.System.Logger.Level.WARNING;
|
||||
@@ -29,6 +32,8 @@ import de.srsoftware.umbrella.core.model.*;
|
||||
import de.srsoftware.umbrella.core.model.Task;
|
||||
import de.srsoftware.umbrella.core.model.Token;
|
||||
import de.srsoftware.umbrella.core.model.UmbrellaUser;
|
||||
import de.srsoftware.umbrella.messagebus.events.Event;
|
||||
import de.srsoftware.umbrella.messagebus.events.TaskEvent;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -37,6 +42,22 @@ import org.json.JSONObject;
|
||||
|
||||
public class TaskModule extends BaseHandler implements TaskService {
|
||||
|
||||
private static class TaggedTask extends Task{
|
||||
private final Collection<String> tags;
|
||||
|
||||
public TaggedTask(Task task, Collection<String> tags) {
|
||||
super(task.id(), task.projectId(), task.parentTaskId(), task.name(), task.description(), task.status(), task.estimatedTime(), task.start(), task.dueDate(), task.showClosed(), task.noIndex(), task.members(), task.priority());
|
||||
this.tags = tags;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> toMap() {
|
||||
var map = super.toMap();
|
||||
map.put(TAGS,tags);
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
private final TaskDb taskDb;
|
||||
|
||||
public TaskModule(Configuration config) throws UmbrellaException {
|
||||
@@ -60,6 +81,7 @@ public class TaskModule extends BaseHandler implements TaskService {
|
||||
taskDb.delete(task);
|
||||
noteService().deleteEntity(TASK, "" + taskId);
|
||||
tagService().deleteEntity(TASK, taskId);
|
||||
messageBus().dispatch(new TaskEvent(user,task,Event.EventType.DELETE));
|
||||
return sendContent(ex, Map.of(DELETED, taskId));
|
||||
}
|
||||
|
||||
@@ -258,6 +280,16 @@ public class TaskModule extends BaseHandler implements TaskService {
|
||||
return taskList;
|
||||
}
|
||||
|
||||
private boolean newParentIsSubtask(Task task, long newParent) {
|
||||
var parent = taskDb.load(newParent);
|
||||
while (parent != null) {
|
||||
if (task.id() == parent.id()) return true;
|
||||
if (parent.parentTaskId() == null) break;
|
||||
parent = taskDb.load(parent.parentTaskId());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Map<String, Object> placeInTree(Task task, HashMap<Long, Map<String, Object>> taskTree, Map<Long, Task> taskMap) {
|
||||
var mappedTask = task.toMap();
|
||||
if (task.parentTaskId() != null) {
|
||||
@@ -311,20 +343,11 @@ public class TaskModule extends BaseHandler implements TaskService {
|
||||
if (json.has(NEW_MEMBER) && json.get(NEW_MEMBER) instanceof Number num) addMember(task, num.longValue());
|
||||
if (json.has(PARENT_TASK_ID) && json.get(PARENT_TASK_ID) instanceof Number ptid && newParentIsSubtask(task, ptid.longValue())) throw forbidden("Task must not be sub-task of itself.");
|
||||
taskDb.save(task.patch(json));
|
||||
|
||||
var tagList = tagService().getTags(TASK, taskId, user);
|
||||
messageBus().dispatch(new TaskEvent(user,new TaggedTask(task,tagList), UPDATE));
|
||||
return sendContent(ex, task);
|
||||
}
|
||||
|
||||
private boolean newParentIsSubtask(Task task, long newParent) {
|
||||
var parent = taskDb.load(newParent);
|
||||
while (parent != null) {
|
||||
if (task.id() == parent.id()) return true;
|
||||
if (parent.parentTaskId() == null) break;
|
||||
parent = taskDb.load(parent.parentTaskId());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean postNewTask(UmbrellaUser user, HttpExchange ex) throws IOException {
|
||||
var json = json(ex);
|
||||
if (!(json.has(PROJECT_ID) && json.get(PROJECT_ID) instanceof Number pid)) throw missingFieldException(PROJECT_ID);
|
||||
@@ -381,7 +404,10 @@ public class TaskModule extends BaseHandler implements TaskService {
|
||||
if ((tagList == null || tagList.isEmpty()) && parentTask != null) tagList = tagService().getTags(TASK, parentTask.id(), user);
|
||||
if ((tagList == null || tagList.isEmpty())) tagList = tagService().getTags(PROJECT, projectId, user);
|
||||
if (tagList != null && !tagList.isEmpty()) tagService().save(TASK, task.id(), null, tagList);
|
||||
return sendContent(ex, loadMembers(task));
|
||||
task = loadMembers(task);
|
||||
|
||||
messageBus().dispatch(new TaskEvent(user,new TaggedTask(task,tagList), CREATE));
|
||||
return sendContent(ex, task);
|
||||
}
|
||||
|
||||
private boolean postSearch(UmbrellaUser user, HttpExchange ex) throws IOException {
|
||||
@@ -420,23 +446,6 @@ public class TaskModule extends BaseHandler implements TaskService {
|
||||
return sendEmptyResponse(HTTP_NOT_IMPLEMENTED, ex);
|
||||
}
|
||||
|
||||
private static class TaggedTask extends Task{
|
||||
|
||||
private final Collection<String> tags;
|
||||
|
||||
public TaggedTask(Task task, Collection<String> tags) {
|
||||
super(task.id(), task.projectId(), task.parentTaskId(), task.name(), task.description(), task.status(), task.estimatedTime(), task.start(), task.dueDate(), task.showClosed(), task.noIndex(), task.members(), task.priority());
|
||||
this.tags = tags;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> toMap() {
|
||||
var map = super.toMap();
|
||||
map.put(TAGS,tags);
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
private Map<Long, Task> addTags(Map<Long, Task> taskList, Map<Long, ? extends Collection<String>> tags) {
|
||||
return taskList.values().stream().map(task -> new TaggedTask(task, tags.get(task.id()))).collect(Collectors.toMap(Task::id, t -> t));
|
||||
}
|
||||
|
||||
@@ -173,6 +173,7 @@
|
||||
"logout_user": "{user} abmelden",
|
||||
|
||||
"markdown_code": "Markdown-Code",
|
||||
"Markdown has been copied to clipboard!": "Markdown wurde in die Zwischenablage kopiert!",
|
||||
"markdown_supported": "Markdown & <a target=\"_blank\" href=\"https://plantuml.com\">Plantuml</a> nutzbar!",
|
||||
"MANAGE_LOGIN_SERVICES": "Login-Services verwalten",
|
||||
"member": "Mitarbeiter",
|
||||
@@ -205,6 +206,7 @@
|
||||
"month": "Monat",
|
||||
"move_to_top": "nach ganz oben bewegen",
|
||||
"must_not_be_empty": "darf nicht leer sein",
|
||||
"my files": "Meine Dateien",
|
||||
|
||||
"name": "Name",
|
||||
"net_price": "Nettopreis",
|
||||
@@ -343,6 +345,9 @@
|
||||
"user_list": "Benutzer-Liste",
|
||||
"user_module" : "Umbrella User-Verwaltung",
|
||||
"users": "Benutzer",
|
||||
"user_created_entity": "{user} hat \"{entity}\" angelegt",
|
||||
"user_deleted_entity": "{user} hat \"{entity}\" gelöscht",
|
||||
"user_updated_entity": "{user} hat \"{entity}\" bearbeitet",
|
||||
|
||||
"website": "Website",
|
||||
"welcome" : "Willkommen, {0}",
|
||||
|
||||
@@ -173,6 +173,7 @@
|
||||
"logout_user": "logout {user}",
|
||||
|
||||
"markdown_code": "Markdown-Code",
|
||||
"Markdown has been copied to clipboard!": "Markdown has been copied to clipboard!",
|
||||
"markdown_supported": "Markdown & <a target=\"_blank\" href=\"https://plantuml.com\">Plantuml</a> supported!",
|
||||
"MANAGE_LOGIN_SERVICES": "manage login services",
|
||||
"member": "member",
|
||||
@@ -205,6 +206,7 @@
|
||||
"month": "month",
|
||||
"move_to_top": "move to top level",
|
||||
"must_not_be_empty": "must not be empty",
|
||||
"my files": "my files",
|
||||
|
||||
"name": "Name",
|
||||
"net_price": "net price",
|
||||
|
||||
@@ -153,6 +153,10 @@ li.task > button{
|
||||
display: none;
|
||||
}
|
||||
|
||||
li.task > button:nth-child(2){
|
||||
display: initial;
|
||||
}
|
||||
|
||||
li.task{
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
@@ -81,6 +81,11 @@ tr:hover .taglist .tag button {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.info{
|
||||
background: orange;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.kanban .add_task,
|
||||
.kanban .box,
|
||||
.kanban .head,
|
||||
|
||||
@@ -168,6 +168,15 @@ td, tr{
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.info {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
font-size: 14px;
|
||||
z-index: 200;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.warn {
|
||||
padding: 5px;
|
||||
border-radius: 6px;
|
||||
@@ -231,6 +240,10 @@ li.task > button{
|
||||
display: none;
|
||||
}
|
||||
|
||||
li.task > button:nth-child(2){
|
||||
display: initial;
|
||||
}
|
||||
|
||||
li.task{
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ body {
|
||||
code {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
border: 1px solid;
|
||||
border-radius: 4px;
|
||||
@@ -152,6 +153,10 @@ li.task > button{
|
||||
display: none;
|
||||
}
|
||||
|
||||
li.task > button:nth-child(2){
|
||||
display: initial;
|
||||
}
|
||||
|
||||
li.task{
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
@@ -268,8 +268,10 @@ public class SqliteDb extends BaseDb implements WikiDb {
|
||||
@Override
|
||||
public WikiPage save(WikiPage page) {
|
||||
try {
|
||||
if (page.isDirty(CONTENT) || page.isDirty(ID) || page.isDirty(TITLE)) insertInto(TABLE_PAGES,ID,VERSION,TITLE,CONTENT)
|
||||
.values(page.id(),page.version(),page.title(),page.content()).execute(db).close();
|
||||
if (page.isDirty(CONTENT) || page.isDirty(ID) || page.isDirty(TITLE)) {
|
||||
insertInto(TABLE_PAGES,ID,VERSION,TITLE,CONTENT).values(page.id(),page.version(),page.title(),page.content()).execute(db).close();
|
||||
page.versions().add(page.version());
|
||||
}
|
||||
if (page.isDirty(MEMBERS)){
|
||||
Query.delete().from(TABLE_PAGES_USERS).where(PAGE_ID, equal(page.id())).where(USER_ID,Condition.notIn(page.members().keySet().toArray())).execute(db);
|
||||
var query = replaceInto(TABLE_PAGES_USERS,PAGE_ID,USER_ID,PERMISSIONS);
|
||||
|
||||
Reference in New Issue
Block a user