Compare commits

..

44 Commits

Author SHA1 Message Date
StephanRichter cf485055a6 improved file module GUI: now pushing a notification when markdown was copied
Build Docker Image / Docker-Build (push) Successful in 2m31s
Build Docker Image / Clean-Registry (push) Successful in 1s
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-16 15:48:56 +01:00
StephanRichter 968e5bfb95 improved file module GUI: files and directories are now sorted
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-16 09:25:32 +01:00
StephanRichter 669853352e Merge branch 'bugfix/doc_template'
Build Docker Image / Docker-Build (push) Successful in 2m31s
Build Docker Image / Clean-Registry (push) Successful in 2s
2026-01-12 22:46:30 +01:00
StephanRichter bdb3443240 added another fix:
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
StephanRichter 43ebb241e8 trying to achieve correct display update on page content update – I have a feeling that this still doesn`t work reliably
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
StephanRichter 1187956625 Merge branch 'bugfix/time_filtered_by_prj'
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
StephanRichter fe0068f5ed added condition to run restart step only on main branch
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
StephanRichter e92a4bedb9 working on curl request
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
StephanRichter 11d14afb00 working on curl request
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
StephanRichter 77e546bd4b fixed typo
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-05 13:54:12 +01:00
StephanRichter beb58bbc36 added remote call to restart vj.srsoftware.de
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
StephanRichter f4adf2ca3c bugfix: altered patch key for updating template in document
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-05 10:34:42 +01:00
StephanRichter 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
StephanRichter b17275a623 dropped workflow branch from trigger list
Build Docker Image / Docker-Build (push) Successful in 2m40s
Build Docker Image / Clean-Registry (push) Successful in 4s
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-03 23:54:41 +01:00
StephanRichter c1adce5a9e overhauling image creation and management
Build Docker Image / Docker-Build (push) Successful in 2m31s
Build Docker Image / Clean-Registry (push) Successful in 3s
2026-01-03 23:53:20 +01:00
StephanRichter a278f73c2e bugfix: forgot to load date
Build Docker Image / Docker-Build (push) Failing after 2m29s
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-03 21:41:51 +01:00
StephanRichter 218d14090c improved date-tagging
Build Docker Image / Docker-Build (push) Has been cancelled
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-03 21:37:39 +01:00
StephanRichter 6f2bbdf635 trying to pass date via tmp
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-03 21:32:53 +01:00
StephanRichter 8cff93c469 dropped reatagging, adding date tag
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-03 21:23:23 +01:00
StephanRichter 1efed60696 working on image retagging
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-03 21:13:36 +01:00
StephanRichter c6dac692de fixed syntax
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-03 21:07:34 +01:00
StephanRichter 202d7deeb9 trying to re-tag image
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-03 21:06:31 +01:00
StephanRichter fc2e484aec added dev to branches that trigger commit
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-03 20:41:51 +01:00
StephanRichter bedb275fe3 disabled build on every push
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-03 18:15:39 +01:00
StephanRichter ff9b520449 Started workflow definition
Build Docker Image / Docker-Build (push) Successful in 2m55s
2026-01-03 18:13:57 +01:00
StephanRichter 2f4c939e93 created workflow file
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-03 15:47:40 +01:00
StephanRichter e596b0cdad starting Dockerfile for Git workflow
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-01-03 15:43:11 +01:00
StephanRichter e50702d0b3 Merge branch 'main' into module/messagebus 2025-12-28 14:10:18 +01:00
StephanRichter f5cd51bc3b improving CSS
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-12-28 14:09:59 +01:00
StephanRichter c05f5110e4 working on project list update by event 2025-12-22 14:42:41 +01:00
StephanRichter 2d4def4265 fixing gradle bug, cleaning up
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-12-20 18:55:19 +01:00
StephanRichter 18c7965b2e working on task update by event 2025-12-20 17:09:06 +01:00
StephanRichter f6813d8a75 working on task update by event 2025-12-20 16:35:47 +01:00
StephanRichter c05f24cd7c finished project tree update on task edit, added update of task view on task edit 2025-12-20 16:29:16 +01:00
StephanRichter 5a1c8e1438 working on project tree update upon task creation/deletion/update 2025-12-20 15:11:18 +01:00
StephanRichter 711addd75c updating kanban on
* task creation
* task update
* task deletion
2025-12-20 13:51:06 +01:00
StephanRichter 78cae4644d simplified kanban:
Before this change tasks were resorted on drop. By introduction of server-sent events, this is no longer required, as resorting also happens triggered by the server side.
2025-12-20 13:21:17 +01:00
StephanRichter 851df52458 re-ordering methods 2025-12-20 13:16:14 +01:00
StephanRichter bd2fb255d2 first working version with event reception in kanban 2025-12-20 00:43:01 +01:00
StephanRichter 3b3803dafa working on event reception in Kanban 2025-12-19 16:07:30 +01:00
StephanRichter bad244ef16 working on message bus
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-12-19 14:06:31 +01:00
StephanRichter 3d31ac90a0 preparing message bus 2025-12-19 13:33:08 +01:00
StephanRichter 73751c1ea2 preparing message bus 2025-12-19 08:42:47 +01:00
StephanRichter a924f25f51 preparing event bus
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2025-12-16 15:26:39 +01:00
47 changed files with 1454 additions and 208 deletions
+76
View File
@@ -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
View File
@@ -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
View File
@@ -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
+2
View File
@@ -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
View File
@@ -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")
+6
View File
@@ -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
View File
@@ -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>
+675
View File
@@ -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",
+2 -2
View File
@@ -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>
+47 -33
View File
@@ -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 = `![${parts.pop()}](/api/files${file})`;
let path = `/api/files${file}`;
path = encodeURI(path);
let md = `![${parts.pop()}](${path})`;
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}
+117 -73
View File
@@ -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>
+15 -5
View File
@@ -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>
+43 -4
View File
@@ -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 -4
View File
@@ -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 -3
View File
@@ -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>
+23 -16
View File
@@ -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>
+3 -4
View File
@@ -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 />
+5 -5
View File
@@ -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>
+8
View File
@@ -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',
+1 -1
View File
@@ -15,5 +15,5 @@ export async function warn(msg){
export function yikes(){
messages.error = null;
messages.warn = null;
messages.warning = null;
}
+1
View File
@@ -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);
}
+1
View File
@@ -2,6 +2,7 @@ rootProject.name = "Umbrella25"
include("backend")
include("bookmark")
include("bus")
include("company")
include("contact")
include("core")
+2 -1
View File
@@ -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));
}
+5
View File
@@ -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}",
+2
View File
@@ -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);