Compare commits

...

45 Commits

Author SHA1 Message Date
2e858e8506 bugfix: Task.tags failed when null was passed
All checks were successful
Build Docker Image / Docker-Build (push) Successful in 2m11s
Build Docker Image / Clean-Registry (push) Successful in 3s
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-12 23:17:22 +01:00
1c104af4a6 enabled debugging in dev branch
All checks were successful
Build Docker Image / Docker-Build (push) Successful in 2m17s
Build Docker Image / Clean-Registry (push) Successful in 2s
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-12 23:08:00 +01:00
fafe20e9e2 Merge branch 'module/journal' into dev
All checks were successful
Build Docker Image / Docker-Build (push) Successful in 2m14s
Build Docker Image / Clean-Registry (push) Successful in 3s
2026-01-12 22:41:29 +01:00
3524bae1d8 improved diff for journal
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-12 22:40:38 +01:00
83c19a7799 fixed Docker build
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-12 21:25:28 +01:00
214eb652e1 implemented some events from wiki module
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-12 09:15:54 +01:00
a990903e3d Merge branch 'bugfix/wikipage' into dev
All checks were successful
Build Docker Image / Docker-Build (push) Successful in 4m26s
Build Docker Image / Clean-Registry (push) Successful in 18s
2026-01-10 22:47:12 +01:00
bdb3443240 added another fix:
All checks were successful
Build Docker Image / Docker-Build (push) Successful in 2m36s
Build Docker Image / Clean-Registry (push) Successful in 2s
- public wiki pages now show login form for guests
- private wiki pages now also show when opened via url (i.e. not by link within umbrella)

Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-10 22:45:44 +01:00
a9a518e508 Merge branch 'module/journal' into dev 2026-01-10 22:26:24 +01:00
9d35952949 implemented event-based page-refresh for wiki pages 2026-01-10 22:19:29 +01:00
c3b49cf032 Merge branch 'main' into module/journal 2026-01-10 21:22:40 +01:00
934aa9bc89 Merge branch 'bugfix/wikipage' into dev
All checks were successful
Build Docker Image / Docker-Build (push) Successful in 4m57s
Build Docker Image / Clean-Registry (push) Successful in 23s
2026-01-10 15:26:58 +01:00
43ebb241e8 trying to achieve correct display update on page content update – I have a feeling that this still doesn`t work reliably
All checks were successful
Build Docker Image / Docker-Build (push) Successful in 4m48s
Build Docker Image / Clean-Registry (push) Successful in 21s
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-10 15:25:16 +01:00
10ea200a2e refactoring Events for better journal
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-10 01:10:24 +01:00
0dd640de30 working on preparing journal loggin
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-09 13:02:53 +01:00
81dc30359d working on journal module
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-09 08:47:20 +01:00
6668e29923 Merge branch 'main' into module/journal 2026-01-09 08:23:46 +01:00
6406580385 added call to make.eldorado.srsoftware.de
All checks were successful
Build Docker Image / Docker-Build (push) Successful in 3m7s
Build Docker Image / Clean-Registry (push) Successful in 1s
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-05 23:15:50 +01:00
1187956625 Merge branch 'bugfix/time_filtered_by_prj'
Some checks failed
Build Docker Image / Docker-Build (push) Failing after 2m30s
Build Docker Image / Clean-Registry (push) Successful in 1s
2026-01-05 15:02:47 +01:00
fe0068f5ed added condition to run restart step only on main branch
All checks were successful
Build Docker Image / Docker-Build (push) Successful in 2m36s
Build Docker Image / Clean-Registry (push) Successful in 1s
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-05 14:13:47 +01:00
e92a4bedb9 working on curl request
All checks were successful
Build Docker Image / Docker-Build (push) Successful in 2m28s
Build Docker Image / Clean-Registry (push) Successful in 2s
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-05 14:06:32 +01:00
11d14afb00 working on curl request
Some checks failed
Build Docker Image / Docker-Build (push) Failing after 2m25s
Build Docker Image / Clean-Registry (push) Successful in 2s
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-05 14:01:55 +01:00
77e546bd4b fixed typo
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-05 13:54:12 +01:00
beb58bbc36 added remote call to restart vj.srsoftware.de
Some checks failed
Build Docker Image / Docker-Build (push) Failing after 2m24s
Build Docker Image / Clean-Registry (push) Successful in 3s
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-05 13:52:31 +01:00
562c854a5b Merge branch 'bugfix/doc_template' into dev
All checks were successful
Build Docker Image / Docker-Build (push) Successful in 2m35s
Build Docker Image / Clean-Registry (push) Successful in 2s
2026-01-05 10:35:51 +01:00
73994d3a4e Merge branch 'bugfix/time_filtered_by_prj' into dev
All checks were successful
Build Docker Image / Docker-Build (push) Successful in 2m40s
Build Docker Image / Clean-Registry (push) Successful in 3s
2026-01-05 08:49:30 +01:00
dddba981c0 implemented bugfix: selecting times by project id broke layout
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-05 08:48:57 +01:00
ac2f974e5a Merge branch 'workflow' into dev
All checks were successful
Build Docker Image / Docker-Build (push) Successful in 2m31s
Build Docker Image / Clean-Registry (push) Successful in 3s
2026-01-03 23:55:31 +01:00
cd25d23246 Merge branch 'feature/workflow' into dev
All checks were successful
Build Docker Image / Docker-Build (push) Successful in 2m40s
2026-01-03 22:17:39 +01:00
8d2f3ef88e fixed tagging
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-03 22:17:30 +01:00
8e53d3b306 Merge branch 'feature/workflow' into dev
All checks were successful
Build Docker Image / Docker-Build (push) Successful in 41s
2026-01-03 22:14:28 +01:00
bd95c3d0c4 debugging
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-03 22:14:22 +01:00
421a350f57 Merge branch 'feature/workflow' into dev
Some checks failed
Build Docker Image / Docker-Build (push) Has been cancelled
2026-01-03 22:13:31 +01:00
dceb84669b debugging
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-03 22:13:21 +01:00
eaeb625d51 Merge branch 'feature/workflow' into dev
Some checks failed
Build Docker Image / Docker-Build (push) Failing after 2m32s
2026-01-03 22:03:10 +01:00
e980dbf884 working on tag creation
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-03 22:03:04 +01:00
dfa991b90a Merge branch 'feature/workflow' into dev
Some checks failed
Build Docker Image / Docker-Build (push) Failing after 2m31s
2026-01-03 21:41:56 +01:00
3b40250488 Merge branch 'feature/workflow' into dev
Some checks failed
Build Docker Image / Docker-Build (push) Failing after 2m29s
2026-01-03 21:37:50 +01:00
8e8992f534 Merge branch 'feature/workflow' into dev
All checks were successful
Build Docker Image / Docker-Build (push) Successful in 2m39s
2026-01-03 21:32:59 +01:00
076efda195 Merge branch 'feature/workflow' into dev
Some checks failed
Build Docker Image / Docker-Build (push) Failing after 2m46s
2026-01-03 21:23:29 +01:00
5c1e802a6f Merge branch 'feature/workflow' into dev
All checks were successful
Build Docker Image / Docker-Build (push) Successful in 2m42s
2026-01-03 21:13:42 +01:00
750e0f16e1 Merge branch 'feature/workflow' into dev
Some checks failed
Build Docker Image / Docker-Build (push) Failing after 2m43s
2026-01-03 21:07:38 +01:00
c06083476f Merge branch 'feature/workflow' into dev
Some checks failed
Build Docker Image / Docker-Build (push) Failing after 0s
2026-01-03 21:06:39 +01:00
f784ec6109 Merge branch 'main' into dev 2025-12-28 14:10:06 +01:00
a4fffbe91b preparing journal module 2025-12-22 15:12:25 +01:00
35 changed files with 321 additions and 76 deletions

View File

@@ -41,6 +41,11 @@ jobs:
docker push ${{ secrets.REGISTRY_PATH }}/umbrella:${{ gitea.ref_name }} docker push ${{ secrets.REGISTRY_PATH }}/umbrella:${{ gitea.ref_name }}
docker push ${{ secrets.REGISTRY_PATH }}/umbrella:$TAG docker push ${{ secrets.REGISTRY_PATH }}/umbrella:$TAG
- name: Restart umbrella.srsoftware.de
if: github.ref == 'refs/heads/dev'
run: |
curl -X POST -H "Authorization: Bearer ${{ secrets.ELDORADO_MAKE_BEARER }}" -d umbrella_25_start https://make.eldorado.srsoftware.de/launch
Clean-Registry: Clean-Registry:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:

View File

@@ -1,15 +1,15 @@
FROM alpine:3.22 AS svelte_build FROM alpine:3.22 AS svelte_build
RUN apk add npm RUN apk add npm
RUN adduser -Dh /home/svelte svelte RUN adduser -Dh /home/svelte svelte
ADD . /home/svelte/Umbrella ADD frontend /home/svelte/Umbrella/frontend
RUN chown -R svelte /home/svelte/Umbrella RUN chown -R svelte /home/svelte/Umbrella
USER svelte USER svelte
WORKDIR /home/svelte/Umbrella/frontend WORKDIR /home/svelte/Umbrella/frontend
RUN npm install && npm run build RUN npm install && npm run build
FROM alpine AS java_build FROM alpine:3.22 AS java_build
RUN apk add gradle fontconfig font-opensans openjdk21-jre RUN apk add gradle
ADD . /Umbrella ADD . /Umbrella
WORKDIR /Umbrella WORKDIR /Umbrella
COPY --from=svelte_build /home/svelte/Umbrella/frontend/dist web/src/main/resources/web COPY --from=svelte_build /home/svelte/Umbrella/frontend/dist web/src/main/resources/web
@@ -26,4 +26,4 @@ USER umbrella
WORKDIR /home/umbrella WORKDIR /home/umbrella
RUN mkdir .config && ln -s /host/config.json .config/Umbrella.json RUN mkdir .config && ln -s /host/config.json .config/Umbrella.json
EXPOSE 80 EXPOSE 80
CMD java -jar jar/backend.jar CMD java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:9999 -jar jar/backend.jar

View File

@@ -1,7 +1,10 @@
FROM alpine:3.22 FROM alpine:3.22
LABEL Maintainer "Stephan Richter <s.richter@srsoftware.de>" LABEL Maintainer "Stephan Richter <s.richter@srsoftware.de>"
ARG UID=1000
ARG GID=1000
RUN apk add bash npm RUN apk add bash npm
RUN adduser -Dh /home/svelte svelte RUN set -x; addgroup -g $GID svelte
RUN adduser -u $UID -G svelte -Dh /home/svelte svelte
ADD script /opt ADD script /opt
USER svelte USER svelte
WORKDIR /home/svelte WORKDIR /home/svelte

View File

@@ -1,16 +1,16 @@
default: devel default: devel
build: image build: image
docker run --name svelte-build \ podman run --name svelte-build \
--rm \ --rm \
-v ../frontend:/home/svelte/frontend \ -v ../frontend:/home/svelte/frontend \
-ti svelte /opt/svelte-build -ti svelte /opt/svelte-build
image: image:
docker build -t svelte . podman build --build-arg UID=$$(id -u) --build-arg GID=$$(id -g) -t svelte .
devel: image devel: image
-docker rm -f svelte -podman rm -f svelte
docker run --name svelte \ podman run --name svelte \
-v ../frontend:/home/svelte/frontend \ -v ../frontend:/home/svelte/frontend \
-p 5173:5173 \ -p 5173:5173 \
-ti svelte /opt/svelte-init -ti svelte /opt/svelte-init

View File

@@ -18,6 +18,7 @@ dependencies{
implementation(project(":core")) implementation(project(":core"))
implementation(project(":documents")) implementation(project(":documents"))
implementation(project(":files")) implementation(project(":files"))
implementation(project(":journal"))
implementation(project(":legacy")) implementation(project(":legacy"))
implementation(project(":markdown")) implementation(project(":markdown"))
implementation(project(":messages")) implementation(project(":messages"))
@@ -52,6 +53,7 @@ tasks.jar {
":core:jar", ":core:jar",
":documents:jar", ":documents:jar",
":files:jar", ":files:jar",
":journal:jar",
":legacy:jar", ":legacy:jar",
":markdown:jar", ":markdown:jar",
":messages:jar", ":messages:jar",

View File

@@ -16,6 +16,7 @@ import de.srsoftware.umbrella.core.Util;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.documents.DocumentApi; import de.srsoftware.umbrella.documents.DocumentApi;
import de.srsoftware.umbrella.files.FileModule; import de.srsoftware.umbrella.files.FileModule;
import de.srsoftware.umbrella.journal.JournalModule;
import de.srsoftware.umbrella.legacy.*; import de.srsoftware.umbrella.legacy.*;
import de.srsoftware.umbrella.markdown.MarkdownApi; import de.srsoftware.umbrella.markdown.MarkdownApi;
import de.srsoftware.umbrella.message.MessageSystem; import de.srsoftware.umbrella.message.MessageSystem;
@@ -64,6 +65,7 @@ public class Application {
var server = HttpServer.create(new InetSocketAddress(port), 0); var server = HttpServer.create(new InetSocketAddress(port), 0);
try { try {
new Translations().bindPath("/api/translations").on(server); new Translations().bindPath("/api/translations").on(server);
new JournalModule(config).bindPath("/api/journal").on(server);
new MessageApi().bindPath("/api/bus").on(server); new MessageApi().bindPath("/api/bus").on(server);
new MessageSystem(config); new MessageSystem(config);
new UserModule(config).bindPath("/api/user").on(server); new UserModule(config).bindPath("/api/user").on(server);

View File

@@ -46,7 +46,7 @@ subprojects {
implementation("de.srsoftware:tools.mime:1.1.4") implementation("de.srsoftware:tools.mime:1.1.4")
implementation("de.srsoftware:tools.logging:1.3.2") implementation("de.srsoftware:tools.logging:1.3.2")
implementation("de.srsoftware:tools.optionals:1.0.0") implementation("de.srsoftware:tools.optionals:1.0.0")
implementation("de.srsoftware:tools.util:2.0.4") implementation("de.srsoftware:tools.util:2.1.1")
implementation("org.json:json:20240303") implementation("org.json:json:20240303")
} }

View File

@@ -4,5 +4,5 @@ package de.srsoftware.umbrella.messagebus;
import de.srsoftware.umbrella.messagebus.events.Event; import de.srsoftware.umbrella.messagebus.events.Event;
public interface EventListener { public interface EventListener {
void onEvent(Event event); void onEvent(Event<?> event);
} }

View File

@@ -7,7 +7,7 @@ import de.srsoftware.umbrella.messagebus.events.Event;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.util.LinkedList; import java.util.LinkedList;
public class EventQueue extends LinkedList<Event> implements AutoCloseable, EventListener { public class EventQueue extends LinkedList<Event<?>> implements AutoCloseable, EventListener {
private final InetSocketAddress addr; private final InetSocketAddress addr;
@@ -29,7 +29,7 @@ public class EventQueue extends LinkedList<Event> implements AutoCloseable, Even
} }
@Override @Override
public void onEvent(Event event) { public void onEvent(Event<?> event) {
System.getLogger(addr.toString()).log(System.Logger.Level.INFO,"adding event to queue of {1}: {0}",event.eventType(),addr); System.getLogger(addr.toString()).log(System.Logger.Level.INFO,"adding event to queue of {1}: {0}",event.eventType(),addr);
add(event); add(event);
} }

View File

@@ -6,8 +6,8 @@ import java.util.HashSet;
import java.util.Set; import java.util.Set;
public class MessageBus { public class MessageBus {
private static MessageBus SINGLETON = new MessageBus(); private static final MessageBus SINGLETON = new MessageBus();
private Set<EventListener> listeners = new HashSet<>(); private final Set<EventListener> listeners = new HashSet<>();
private MessageBus(){} private MessageBus(){}

View File

@@ -1,39 +1,75 @@
/* © SRSoftware 2025 */ /* © SRSoftware 2025 */
package de.srsoftware.umbrella.messagebus.events; package de.srsoftware.umbrella.messagebus.events;
import static de.srsoftware.umbrella.core.Constants.USER; import static de.srsoftware.umbrella.core.Constants.*;
import static java.util.Optional.*;
import de.srsoftware.tools.Diff;
import de.srsoftware.tools.Mappable; import de.srsoftware.tools.Mappable;
import de.srsoftware.umbrella.core.model.UmbrellaUser; import de.srsoftware.umbrella.core.model.UmbrellaUser;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import org.json.JSONObject; import org.json.JSONObject;
public abstract class Event<Payload extends Mappable> { public abstract class Event<Payload extends Mappable> {
public enum EventType { public enum EventType {
CREATE, CREATE,
UPDATE, UPDATE,
DELETE; DELETE;
} }
private UmbrellaUser initiator;
private String realm; private final UmbrellaUser initiator;
private Payload payload; private final String module;
private EventType eventType; private final Payload payload;
public Event(UmbrellaUser initiator, String realm, Payload payload, EventType type){ private final EventType eventType;
private final Map<String, Object> oldData;
public Event(UmbrellaUser initiator, String module, Payload payload, EventType type){
this.initiator = initiator; this.initiator = initiator;
this.realm = realm; this.module = module;
this.payload = payload; this.payload = payload;
this.eventType = type; this.eventType = type;
this.oldData = null;
} }
public Event(UmbrellaUser initiator, String module, Payload payload, Map<String, Object> oldData){
this.initiator = initiator;
this.module = module;
this.payload = payload;
this.eventType = EventType.UPDATE;
this.oldData = oldData;
}
public abstract String describe();
private Map<String, Object> dropMarkdown(Map<String, Object> map) {
var result = new HashMap<String, Object>();
for (var entry : map.entrySet()){
var v = entry.getValue();
if (v instanceof Map<?,?> m && m.containsKey(RENDERED) && m.get(SOURCE) instanceof String s) v=s;
result.put(entry.getKey(),v);
}
return result;
}
public Optional<String> diff(){
return oldData == null ? empty() : of(Diff.MapDiff.diff(dropMarkdown(oldData),dropMarkdown(payload.toMap())));
}
public String eventType(){ public String eventType(){
return eventType.toString(); return eventType.toString();
} }
public abstract boolean isIntendedFor(UmbrellaUser user); public abstract boolean isIntendedFor(UmbrellaUser user);
public UmbrellaUser initiator(){
return initiator;
}
public String json(){ public String json(){
Class<?> clazz = payload.getClass(); Class<?> clazz = payload.getClass();
{ // get the highest superclass that is not object { // get the highest superclass that is not object
@@ -48,6 +84,10 @@ public abstract class Event<Payload extends Mappable> {
return new JSONObject(map).toString(); return new JSONObject(map).toString();
} }
public String module(){
return module;
}
public Payload payload(){ public Payload payload(){
return payload; return payload;
} }

View File

@@ -5,6 +5,7 @@ import static de.srsoftware.umbrella.core.Constants.PROJECT;
import de.srsoftware.umbrella.core.model.Project; import de.srsoftware.umbrella.core.model.Project;
import de.srsoftware.umbrella.core.model.UmbrellaUser; import de.srsoftware.umbrella.core.model.UmbrellaUser;
import java.util.Map;
public class ProjectEvent extends Event<Project>{ public class ProjectEvent extends Event<Project>{
@@ -12,6 +13,15 @@ public class ProjectEvent extends Event<Project>{
super(initiator, PROJECT, project, type); super(initiator, PROJECT, project, type);
} }
public ProjectEvent(UmbrellaUser initiator, Project project, Map<String, Object> oldData){
super(initiator, PROJECT, project, oldData);
}
@Override
public String describe() {
return diff().orElse("[TODO: ProjectEvent.describe]");
}
@Override @Override
public boolean isIntendedFor(UmbrellaUser user) { public boolean isIntendedFor(UmbrellaUser user) {
for (var member : payload().members().values()){ for (var member : payload().members().values()){

View File

@@ -5,6 +5,7 @@ import static de.srsoftware.umbrella.core.Constants.TASK;
import de.srsoftware.umbrella.core.model.Task; import de.srsoftware.umbrella.core.model.Task;
import de.srsoftware.umbrella.core.model.UmbrellaUser; import de.srsoftware.umbrella.core.model.UmbrellaUser;
import java.util.Map;
public class TaskEvent extends Event<Task>{ public class TaskEvent extends Event<Task>{
@@ -12,6 +13,15 @@ public class TaskEvent extends Event<Task>{
super(initiator, TASK, task, type); super(initiator, TASK, task, type);
} }
public TaskEvent(UmbrellaUser initiator, Task task, Map<String, Object> oldData){
super(initiator, TASK, task, oldData);
}
@Override
public String describe() {
return diff().orElse("[TODO: TaskEvent.describe()]");
}
@Override @Override
public boolean isIntendedFor(UmbrellaUser user) { public boolean isIntendedFor(UmbrellaUser user) {
for (var member : payload().members().values()){ for (var member : payload().members().values()){

View File

@@ -0,0 +1,32 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.messagebus.events;
import static de.srsoftware.umbrella.core.Constants.WIKI;
import de.srsoftware.umbrella.core.model.UmbrellaUser;
import de.srsoftware.umbrella.core.model.WikiPage;
import java.util.Map;
public class WikiEvent extends Event<WikiPage>{
public WikiEvent(UmbrellaUser initiator, WikiPage page, EventType type){
super(initiator, WIKI, page, type);
}
public WikiEvent(UmbrellaUser initiator, WikiPage page, Map<String, Object> oldData){
super(initiator, WIKI, page, oldData);
}
@Override
public String describe() {
return diff().orElse("[TODO: WikiEvent.describe()]");
}
@Override
public boolean isIntendedFor(UmbrellaUser user) {
for (var member : payload().members().values()){
if (member.user().equals(user)) return true;
}
return false;
}
}

View File

@@ -75,5 +75,4 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255)
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
} }

View File

@@ -8,10 +8,7 @@ public class Constants {
private Constants(){} private Constants(){}
public static final String ACTION = "action";
public static final String ADDRESS = "address"; public static final String ADDRESS = "address";
public static final String ALLOWED_STATES = "allowed_states"; public static final String ALLOWED_STATES = "allowed_states";
public static final String ATTACHMENTS = "attachments"; public static final String ATTACHMENTS = "attachments";
@@ -163,4 +160,5 @@ public class Constants {
public static final String VERSION = "version"; public static final String VERSION = "version";
public static final String VERSIONS = "versions"; public static final String VERSIONS = "versions";
public static final String WIKI = "wiki";
} }

View File

@@ -8,6 +8,7 @@ import static java.lang.System.Logger.Level.ERROR;
import static java.lang.System.Logger.Level.WARNING; import static java.lang.System.Logger.Level.WARNING;
import static java.net.HttpURLConnection.HTTP_FORBIDDEN; import static java.net.HttpURLConnection.HTTP_FORBIDDEN;
import static java.net.HttpURLConnection.HTTP_NOT_FOUND; import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
import static java.text.MessageFormat.format;
public class UmbrellaException extends RuntimeException{ public class UmbrellaException extends RuntimeException{
@@ -43,6 +44,11 @@ public class UmbrellaException extends RuntimeException{
return new UmbrellaException(HTTP_FORBIDDEN,message,fills); return new UmbrellaException(HTTP_FORBIDDEN,message,fills);
} }
@Override
public String getMessage() {
return format(super.getMessage(),fills);
}
public static UmbrellaException invalidFieldException(String field, String expected){ public static UmbrellaException invalidFieldException(String field, String expected){
return new UmbrellaException(HTTP_UNPROCESSABLE, ERROR_INVALID_FIELD, field, expected); return new UmbrellaException(HTTP_UNPROCESSABLE, ERROR_INVALID_FIELD, field, expected);
} }

View File

@@ -27,7 +27,7 @@ public class Task implements Mappable {
private boolean noIndex, showClosed; private boolean noIndex, showClosed;
private final Map<Long, Member> members; private final Map<Long, Member> members;
private final Set<String> dirtyFields = new HashSet<>(); private final Set<String> dirtyFields = new HashSet<>();
private final Set<String> tags = new HashSet<>();
public Task (long id, long projectId, Long parentTaskId, String name, String description, int status, Double estimatedTime, LocalDate start, LocalDate dueDate, boolean showClosed, boolean noIndex, Map<Long,Member> members, int priority){ public Task (long id, long projectId, Long parentTaskId, String name, String description, int status, Double estimatedTime, LocalDate start, LocalDate dueDate, boolean showClosed, boolean noIndex, Map<Long,Member> members, int priority){
this.id = id; this.id = id;
@@ -218,6 +218,16 @@ public class Task implements Mappable {
return status; return status;
} }
public Task tags(Collection<String> newValue){
tags.clear();
if (newValue != null) tags.addAll(newValue);
return this;
}
public Set<String> tags(){
return tags;
}
@Override @Override
public Map<String, Object> toMap() { public Map<String, Object> toMap() {
var map = new HashMap<String,Object>(); var map = new HashMap<String,Object>();
@@ -240,7 +250,7 @@ public class Task implements Mappable {
map.put(REQUIRED_TASKS_IDS,requiredTasksIds); map.put(REQUIRED_TASKS_IDS,requiredTasksIds);
map.put(SHOW_CLOSED,showClosed); map.put(SHOW_CLOSED,showClosed);
map.put(TOTAL_PRIO,totalPrio()); map.put(TOTAL_PRIO,totalPrio());
map.put(TAGS,tags);
return map; return map;
} }

View File

@@ -18,7 +18,7 @@ public class WikiPage implements Mappable {
private final long id; private final long id;
private String title; private String title;
private int version; 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 final Map<Long,Member> members = new HashMap<>();
private String content; private String content;
private Set<String> dirtyFields = new HashSet<>(); private Set<String> dirtyFields = new HashSet<>();
@@ -157,7 +157,7 @@ public class WikiPage implements Mappable {
return version; return version;
} }
public List<Integer> versions(){ public Set<Integer> versions(){
return versions; return versions;
} }
} }

View File

@@ -20,7 +20,7 @@
let projects = {}; let projects = {};
let project_filter = $state(null); let project_filter = $state(null);
if (router.hasQueryParam('project')) project_filter = router.getQueryParam('project'); 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, ...time,
start: display(time.start_time), start: display(time.start_time),
end: display(time.end_time), end: display(time.end_time),
@@ -52,6 +52,7 @@
} }
function calcYearMap(){ function calcYearMap(){
console.log('calcYearMap called');
let result = { let result = {
months : {}, months : {},
years : {} years : {}
@@ -123,7 +124,7 @@
function match_prj_filter(time){ function match_prj_filter(time){
if (!project_filter) return true; if (!project_filter) return true;
for (var tid of time.task_ids){ 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; return false;
} }
@@ -258,7 +259,6 @@
</thead> </thead>
<tbody> <tbody>
{#each sortedTimes as time,line} {#each sortedTimes as time,line}
{#if match_prj_filter(time)}
<tr class={selected[time.id]?'selected':''}> <tr class={selected[time.id]?'selected':''}>
{#if timeMap.years[line]} {#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)} > <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> </td>
{/if} {/if}
</tr> </tr>
{/if}
{/each} {/each}
</tbody> </tbody>
</table> </table>

View File

@@ -1,4 +1,6 @@
<script> <script>
import Login from "../../Components/Login.svelte";
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { useTinyRouter } from 'svelte-tiny-router'; import { useTinyRouter } from 'svelte-tiny-router';
import { api } from '../../urls.svelte'; import { api } from '../../urls.svelte';
@@ -45,3 +47,4 @@
</div> </div>
</div> </div>
{/if} {/if}
<Login />

View File

@@ -1,7 +1,7 @@
<script> <script>
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { useTinyRouter } from 'svelte-tiny-router'; import { useTinyRouter } from 'svelte-tiny-router';
import { api } from '../../urls.svelte'; import { api, eventStream } from '../../urls.svelte';
import { error, yikes } from '../../warn.svelte'; import { error, yikes } from '../../warn.svelte';
import { t } from '../../translations.svelte'; import { t } from '../../translations.svelte';
import { user } from '../../user.svelte'; import { user } from '../../user.svelte';
@@ -13,8 +13,9 @@
import TagList from '../tags/TagList.svelte'; import TagList from '../tags/TagList.svelte';
let detail = $state(false); let detail = $state(false);
let eventSource = null;
let { key, version } = $props(); let { key, version } = $props();
let page = $state(null); let page = $state({});
let router = useTinyRouter(); let router = useTinyRouter();
let members = $state({}); let members = $state({});
let editable = $derived(page.members[user.id].permission.code<4); let editable = $derived(page.members[user.id].permission.code<4);
@@ -27,6 +28,10 @@
return patch({members:newMembers}); return patch({members:newMembers});
} }
function connectToBus(){
eventSource = eventStream(null,handleUpdateEvent,null);
}
async function dropMember(member){ async function dropMember(member){
var id = member.user.id; var id = member.user.id;
let newMembers = JSON.parse(JSON.stringify(page.members)); let newMembers = JSON.parse(JSON.stringify(page.members));
@@ -51,6 +56,11 @@
} }
} }
function handleUpdateEvent(evt){
let json = JSON.parse(evt.data);
if (json.wikipage) loadContent(json.wikipage);
}
function nonMember(json){ function nonMember(json){
return !page.members[json.id]; return !page.members[json.id];
} }
@@ -70,11 +80,15 @@
} }
} }
async function loadContent(res){ function loadContent(json){
json.versions.sort((a,b)=>b-a);
page = { ...json };
}
async function loadJson(res){
if (res.ok){ if (res.ok){
page = null; let json = await res.json();
page = await res.json(); loadContent(json);
page.versions.sort((a,b)=>b-a);
yikes(); yikes();
return true; return true;
} else { } else {
@@ -88,7 +102,7 @@
if (version) path += `/version/${version}`; if (version) path += `/version/${version}`;
const url = api(path); const url = api(path);
const res = await fetch(url,{credentials:'include'}); const res = await fetch(url,{credentials:'include'});
loadContent(res); loadJson(res);
} }
function onclick(e){ function onclick(e){
@@ -105,7 +119,7 @@
method:'PATCH', method:'PATCH',
body:JSON.stringify(data) body:JSON.stringify(data)
}); });
return loadContent(res); return loadJson(res);
} }
async function patchGuestPermissions(ev){ async function patchGuestPermissions(ev){
@@ -130,8 +144,9 @@
} }
$effect(loadPage); $effect(loadPage);
onMount(connectToBus);
</script> </script>
{#if page} {#if page && page.versions}
<div class="wiki page"> <div class="wiki page">
<div class="versions"> <div class="versions">
<span class="version">{t('version')}</span> <span class="version">{t('version')}</span>

7
journal/build.gradle.kts Normal file
View File

@@ -0,0 +1,7 @@
description = "Umbrella : Journal"
dependencies{
implementation(project(":bus"))
implementation(project(":core"))
}

View File

@@ -0,0 +1,9 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.journal;
public class Constants {
public static final String CONFIG_DATABASE = "umbrella.modules.journal.database";
public static final String ERROR_WRITE_EVENT = "Failed to write {0} event of {1} to journal!";
public static final String TABLE_JOURNAL = "journal";
}

View File

@@ -0,0 +1,8 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.journal;
import de.srsoftware.umbrella.messagebus.events.Event;
public interface JournalDb {
void logEvent(Event<?> event);
}

View File

@@ -0,0 +1,34 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.journal;
import static de.srsoftware.umbrella.core.ConnectionProvider.connect;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.missingFieldException;
import static de.srsoftware.umbrella.journal.Constants.CONFIG_DATABASE;
import static de.srsoftware.umbrella.messagebus.MessageBus.messageBus;
import static java.lang.System.Logger.Level.DEBUG;
import de.srsoftware.configuration.Configuration;
import de.srsoftware.umbrella.core.BaseHandler;
import de.srsoftware.umbrella.core.ModuleRegistry;
import de.srsoftware.umbrella.messagebus.EventListener;
import de.srsoftware.umbrella.messagebus.events.Event;
public class JournalModule extends BaseHandler implements EventListener {
private final JournalDb journalDb;
public JournalModule(Configuration config){
super();
var dbFile = config.get(CONFIG_DATABASE).orElseThrow(() -> missingFieldException(CONFIG_DATABASE));
journalDb = new SqliteDb(connect(dbFile));
ModuleRegistry.add(this);
messageBus().register(this);
}
@Override
public void onEvent(Event<?> event) {
LOG.log(DEBUG,"{0} @ {1} (by {2})",event.eventType(),event.module(),event.initiator().name());
journalDb.logEvent(event);
}
}

View File

@@ -0,0 +1,59 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.journal;
import static de.srsoftware.tools.jdbc.Query.insertInto;
import static de.srsoftware.umbrella.core.Constants.*;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.databaseException;
import static de.srsoftware.umbrella.journal.Constants.ERROR_WRITE_EVENT;
import static de.srsoftware.umbrella.journal.Constants.TABLE_JOURNAL;
import static java.text.MessageFormat.format;
import de.srsoftware.umbrella.core.BaseDb;
import de.srsoftware.umbrella.messagebus.events.Event;
import java.sql.Connection;
import java.sql.SQLException;
public class SqliteDb extends BaseDb implements JournalDb{
public SqliteDb(Connection connection) {
super(connection);
}
protected int createTables() {
int currentVersion = createSettingsTable();
switch (currentVersion){
case 0:
createJournalTable();
}
return setCurrentVersion(1);
}
private void createJournalTable() {
var sql = """
CREATE TABLE IF NOT EXISTS {0} (
{1} INTEGER PRIMARY KEY,
{2} INTEGER,
{3} VARCHAR(255) NOT NULL,
{4} VARCHAR(16) NOT NULL,
{5} TEXT
);
""";
sql = format(sql,TABLE_JOURNAL,ID,USER_ID,MODULE,ACTION,DESCRIPTION);
try {
db.prepareStatement(sql).execute();
} catch (SQLException e) {
throw databaseException(ERROR_FAILED_CREATE_TABLE,TABLE_JOURNAL);
}
}
@Override
public void logEvent(Event<?> event) {
try {
insertInto(TABLE_JOURNAL,USER_ID,MODULE,ACTION,DESCRIPTION)
.values(event.initiator().id(), event.module(), event.eventType(), event.describe())
.execute(db).close();
} catch (SQLException e) {
throw databaseException(ERROR_WRITE_EVENT,event.eventType(),event.initiator().name());
}
}
}

View File

@@ -14,7 +14,6 @@ import static de.srsoftware.umbrella.core.model.Status.OPEN;
import static de.srsoftware.umbrella.core.model.Status.PREDEFINED; import static de.srsoftware.umbrella.core.model.Status.PREDEFINED;
import static de.srsoftware.umbrella.messagebus.MessageBus.messageBus; 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.CREATE;
import static de.srsoftware.umbrella.messagebus.events.Event.EventType.UPDATE;
import static de.srsoftware.umbrella.project.Constants.CONFIG_DATABASE; import static de.srsoftware.umbrella.project.Constants.CONFIG_DATABASE;
import static java.lang.Boolean.TRUE; import static java.lang.Boolean.TRUE;
import static java.net.HttpURLConnection.HTTP_BAD_REQUEST; import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
@@ -210,13 +209,14 @@ public class ProjectModule extends BaseHandler implements ProjectService {
private boolean patchProject(HttpExchange ex, long projectId, UmbrellaUser user) throws IOException, UmbrellaException { private boolean patchProject(HttpExchange ex, long projectId, UmbrellaUser user) throws IOException, UmbrellaException {
var project = loadMembers(projectDb.load(projectId)); var project = loadMembers(projectDb.load(projectId));
if (!project.hasMember(user)) throw forbidden("You are not a member of {0}",project.name()); if (!project.hasMember(user)) throw forbidden("You are not a member of {0}",project.name());
var old = project.toMap();
var json = json(ex); var json = json(ex);
if (json.has(DROP_MEMBER) && json.get(DROP_MEMBER) instanceof Number id) dropMember(project,id.longValue()); if (json.has(DROP_MEMBER) && json.get(DROP_MEMBER) instanceof Number id) dropMember(project,id.longValue());
if (json.has(MEMBERS) && json.get(MEMBERS) instanceof JSONObject memberJson) patchMembers(project,memberJson); 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()); if (json.has(NEW_MEMBER) && json.get(NEW_MEMBER) instanceof Number num) addMember(project,num.longValue());
project = projectDb.save(project.patch(json), user); project = projectDb.save(project.patch(json), user);
messageBus().dispatch(new ProjectEvent(user,project, UPDATE)); messageBus().dispatch(new ProjectEvent(user,project, old));
return sendContent(ex,project.toMap()); return sendContent(ex,project.toMap());
} }

View File

@@ -21,3 +21,5 @@ include("translations")
include("user") include("user")
include("web") include("web")
include("wiki") include("wiki")
include("journal")

View File

@@ -13,7 +13,6 @@ import static de.srsoftware.umbrella.core.model.Permission.*;
import static de.srsoftware.umbrella.core.model.Permission.OWNER; import static de.srsoftware.umbrella.core.model.Permission.OWNER;
import static de.srsoftware.umbrella.messagebus.MessageBus.messageBus; 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.CREATE;
import static de.srsoftware.umbrella.messagebus.events.Event.EventType.UPDATE;
import static de.srsoftware.umbrella.project.Constants.PERMISSIONS; import static de.srsoftware.umbrella.project.Constants.PERMISSIONS;
import static de.srsoftware.umbrella.task.Constants.*; import static de.srsoftware.umbrella.task.Constants.*;
import static java.lang.System.Logger.Level.WARNING; import static java.lang.System.Logger.Level.WARNING;
@@ -42,22 +41,6 @@ import org.json.JSONObject;
public class TaskModule extends BaseHandler implements TaskService { 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; private final TaskDb taskDb;
public TaskModule(Configuration config) throws UmbrellaException { public TaskModule(Configuration config) throws UmbrellaException {
@@ -335,6 +318,7 @@ public class TaskModule extends BaseHandler implements TaskService {
private boolean patchTask(HttpExchange ex, long taskId, UmbrellaUser user) throws IOException { private boolean patchTask(HttpExchange ex, long taskId, UmbrellaUser user) throws IOException {
var task = loadMembers(taskDb.load(taskId)); var task = loadMembers(taskDb.load(taskId));
var old = task.toMap();
var member = task.members().get(user.id()); var member = task.members().get(user.id());
if (member == null || member.permission() == READ_ONLY) throw forbidden("You are not a allowed to edit {0}!", task.name()); if (member == null || member.permission() == READ_ONLY) throw forbidden("You are not a allowed to edit {0}!", task.name());
var json = json(ex); var json = json(ex);
@@ -342,9 +326,8 @@ public class TaskModule extends BaseHandler implements TaskService {
if (json.has(MEMBERS) && json.get(MEMBERS) instanceof JSONObject memberJson) patchMembers(task, memberJson); if (json.has(MEMBERS) && json.get(MEMBERS) instanceof JSONObject memberJson) patchMembers(task, memberJson);
if (json.has(NEW_MEMBER) && json.get(NEW_MEMBER) instanceof Number num) addMember(task, num.longValue()); 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."); 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)); task = taskDb.save(task.patch(json)).tags(tagService().getTags(TASK, taskId, user));
var tagList = tagService().getTags(TASK, taskId, user); messageBus().dispatch(new TaskEvent(user, task, old));
messageBus().dispatch(new TaskEvent(user,new TaggedTask(task,tagList), UPDATE));
return sendContent(ex, task); return sendContent(ex, task);
} }
@@ -405,8 +388,8 @@ public class TaskModule extends BaseHandler implements TaskService {
if ((tagList == null || tagList.isEmpty())) tagList = tagService().getTags(PROJECT, projectId, user); if ((tagList == null || tagList.isEmpty())) tagList = tagService().getTags(PROJECT, projectId, user);
if (tagList != null && !tagList.isEmpty()) tagService().save(TASK, task.id(), null, tagList); if (tagList != null && !tagList.isEmpty()) tagService().save(TASK, task.id(), null, tagList);
task = loadMembers(task); task = loadMembers(task);
task.tags(tagList);
messageBus().dispatch(new TaskEvent(user,new TaggedTask(task,tagList), CREATE)); messageBus().dispatch(new TaskEvent(user, task, CREATE));
return sendContent(ex, task); return sendContent(ex, task);
} }
@@ -447,6 +430,6 @@ public class TaskModule extends BaseHandler implements TaskService {
} }
private Map<Long, Task> addTags(Map<Long, Task> taskList, Map<Long, ? extends Collection<String>> tags) { 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)); return taskList.values().stream().map(task -> task.tags(tags.get(task.id()))).collect(Collectors.toMap(Task::id, t -> t));
} }
} }

View File

@@ -9,4 +9,5 @@ tasks.processResources {
from("../frontend/dist") { from("../frontend/dist") {
into("web") into("web")
} }
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
} }

View File

@@ -1,5 +1,6 @@
description = "Umbrella : Wiki" description = "Umbrella : Wiki"
dependencies{ dependencies{
implementation(project(":bus"))
implementation(project(":core")) implementation(project(":core"))
} }

View File

@@ -268,8 +268,10 @@ public class SqliteDb extends BaseDb implements WikiDb {
@Override @Override
public WikiPage save(WikiPage page) { public WikiPage save(WikiPage page) {
try { try {
if (page.isDirty(CONTENT) || page.isDirty(ID) || page.isDirty(TITLE)) insertInto(TABLE_PAGES,ID,VERSION,TITLE,CONTENT) if (page.isDirty(CONTENT) || page.isDirty(ID) || page.isDirty(TITLE)) {
.values(page.id(),page.version(),page.title(),page.content()).execute(db).close(); 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)){ 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); 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); var query = replaceInto(TABLE_PAGES_USERS,PAGE_ID,USER_ID,PERMISSIONS);

View File

@@ -10,6 +10,7 @@ import static de.srsoftware.umbrella.core.Util.mapValues;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*; import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*;
import static de.srsoftware.umbrella.core.model.Permission.EDIT; import static de.srsoftware.umbrella.core.model.Permission.EDIT;
import static de.srsoftware.umbrella.core.model.Permission.READ_ONLY; import static de.srsoftware.umbrella.core.model.Permission.READ_ONLY;
import static de.srsoftware.umbrella.messagebus.MessageBus.messageBus;
import static de.srsoftware.umbrella.wiki.Constants.*; import static de.srsoftware.umbrella.wiki.Constants.*;
import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpExchange;
@@ -20,6 +21,7 @@ import de.srsoftware.umbrella.core.BaseHandler;
import de.srsoftware.umbrella.core.api.WikiService; import de.srsoftware.umbrella.core.api.WikiService;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.core.model.*; import de.srsoftware.umbrella.core.model.*;
import de.srsoftware.umbrella.messagebus.events.WikiEvent;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import java.util.Optional; import java.util.Optional;
@@ -190,10 +192,13 @@ public class WikiModule extends BaseHandler implements WikiService {
private boolean patchPage(Path path, UmbrellaUser user, HttpExchange ex) throws IOException { private boolean patchPage(Path path, UmbrellaUser user, HttpExchange ex) throws IOException {
var id = path.pop(); var id = path.pop();
var page = loadPage(id, null); var page = loadPage(id, null);
var old = page.toMap();
var member = page.members().get(user.id()); var member = page.members().get(user.id());
if (member == null || member.permission() != EDIT) throw forbidden("You are not allowed to edit {0}!",id); if (member == null || member.permission() != EDIT) throw forbidden("You are not allowed to edit {0}!",id);
var json = json(ex); var json = json(ex);
return sendContent(ex,wikiDb.save(page.patch(json, userService()))); page = wikiDb.save(page.patch(json, userService()));
messageBus().dispatch(new WikiEvent(user,page,old));
return sendContent(ex,page);
} }
private boolean postNewPage(String title, UmbrellaUser user, HttpExchange ex) throws IOException { private boolean postNewPage(String title, UmbrellaUser user, HttpExchange ex) throws IOException {