Compare commits

...

18 Commits

Author SHA1 Message Date
StephanRichter 9f3631ecfd added note, that spreadsheet library is being loaded
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-03-25 09:11:17 +01:00
StephanRichter 20d46ea135 improved spreadsheet design
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-03-21 01:27:27 +01:00
StephanRichter 30ebe3f4e7 first working version. need to improve styling (right align for number cells)
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-03-21 00:41:26 +01:00
StephanRichter 87b81756cd almost there:
- spreadsheet is rendered from fenced code block
- source gets updated when spreadsheet is updated

missing: update spreadsheet, when source is updated
2026-03-20 23:16:00 +01:00
StephanRichter b3925bb2b9 first working transition from source to table. next: writing back to source
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-03-20 20:48:05 +01:00
StephanRichter 1eaff233d6 found a solution to lazy-load the spreadsheet js and css
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-03-20 09:13:41 +01:00
StephanRichter a2e2643020 playing with jspreadsheet
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-03-20 08:26:45 +01:00
StephanRichter 62981b22b5 success using jspreadsheet
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-03-18 23:42:01 +01:00
StephanRichter d936c08d35 first success with x-data-spreadsheet
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-03-18 22:55:42 +01:00
StephanRichter 22094e7ccc implemented autocomplete for filter in kanban
Build Docker Image / Docker-Build (push) Successful in 2m22s
Build Docker Image / Clean-Registry (push) Successful in -8s
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-03-17 09:55:11 +01:00
StephanRichter 3927898c9b implemented autocomplete input for tag editor
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-03-17 09:16:43 +01:00
StephanRichter 2fcf024410 implemented mouse action on dropdown
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-03-17 08:28:06 +01:00
StephanRichter 5c0efe5730 overhauled occurences of AutoComplete in PermissionEditor and UserSelector, overhauled Occurences of UserSelector and PermissionEditor
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-03-17 01:10:54 +01:00
StephanRichter 72f897c40c fixed permissionEditor in various locations
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-03-17 00:56:42 +01:00
StephanRichter d3e5897cd5 completed autocomplete box in permission editor for projects
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-03-17 00:35:49 +01:00
StephanRichter ef71cf3b20 first step in refactoring autocomplete: combining input and options
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-03-16 23:11:16 +01:00
StephanRichter e14e37fc9d Merge branch 'feature/bookmark_edit'
Build Docker Image / Docker-Build (push) Successful in 2m16s
Build Docker Image / Clean-Registry (push) Successful in -8s
2026-03-16 21:53:15 +01:00
StephanRichter 3e6ee91041 added display for stock items to TagUses
Build Docker Image / Docker-Build (push) Successful in 2m18s
Build Docker Image / Clean-Registry (push) Successful in -8s
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-03-16 09:12:12 +01:00
40 changed files with 558 additions and 159 deletions
@@ -1,7 +1,6 @@
/* © SRSoftware 2025 */ /* © SRSoftware 2025 */
package de.srsoftware.umbrella.bookmarks; package de.srsoftware.umbrella.bookmarks;
import static de.srsoftware.umbrella.messagebus.events.Event.EventType;
import static de.srsoftware.umbrella.bookmarks.Constants.*; import static de.srsoftware.umbrella.bookmarks.Constants.*;
import static de.srsoftware.umbrella.core.ConnectionProvider.connect; import static de.srsoftware.umbrella.core.ConnectionProvider.connect;
import static de.srsoftware.umbrella.core.ModuleRegistry.tagService; import static de.srsoftware.umbrella.core.ModuleRegistry.tagService;
@@ -29,14 +28,9 @@ import de.srsoftware.umbrella.core.constants.Text;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.core.model.Token; import de.srsoftware.umbrella.core.model.Token;
import de.srsoftware.umbrella.core.model.UmbrellaUser; import de.srsoftware.umbrella.core.model.UmbrellaUser;
import de.srsoftware.umbrella.messagebus.events.BookmarkEvent;
import java.awt.print.Book;
import java.io.IOException; import java.io.IOException;
import java.util.*; import java.util.*;
import de.srsoftware.umbrella.messagebus.events.BookmarkEvent;
import de.srsoftware.umbrella.messagebus.events.Event;
import de.srsoftware.umbrella.messagebus.events.TaskEvent;
import org.json.JSONArray; import org.json.JSONArray;
public class BookmarkApi extends BaseHandler implements BookmarkService { public class BookmarkApi extends BaseHandler implements BookmarkService {
@@ -3,7 +3,6 @@ package de.srsoftware.umbrella.bookmarks;
import de.srsoftware.umbrella.core.model.Bookmark; import de.srsoftware.umbrella.core.model.Bookmark;
import de.srsoftware.umbrella.core.model.UmbrellaUser; import de.srsoftware.umbrella.core.model.UmbrellaUser;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Collection; import java.util.Collection;
import java.util.Map; import java.util.Map;
@@ -12,8 +12,6 @@ import static de.srsoftware.umbrella.core.constants.Field.*;
import static de.srsoftware.umbrella.core.constants.Text.BOOKMARK; import static de.srsoftware.umbrella.core.constants.Text.BOOKMARK;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*; import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*;
import static de.srsoftware.umbrella.messagebus.MessageBus.messageBus; import static de.srsoftware.umbrella.messagebus.MessageBus.messageBus;
import static de.srsoftware.umbrella.messagebus.events.Event.EventType.CREATE;
import static de.srsoftware.umbrella.messagebus.events.Event.EventType.UPDATE;
import static java.text.MessageFormat.format; import static java.text.MessageFormat.format;
import static java.time.ZoneOffset.UTC; import static java.time.ZoneOffset.UTC;
@@ -24,7 +22,6 @@ import de.srsoftware.umbrella.core.model.Translatable;
import de.srsoftware.umbrella.core.model.UmbrellaUser; import de.srsoftware.umbrella.core.model.UmbrellaUser;
import de.srsoftware.umbrella.messagebus.events.BookmarkEvent; import de.srsoftware.umbrella.messagebus.events.BookmarkEvent;
import de.srsoftware.umbrella.messagebus.events.Event; import de.srsoftware.umbrella.messagebus.events.Event;
import java.sql.Connection; import java.sql.Connection;
import java.sql.SQLException; import java.sql.SQLException;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -1,17 +1,16 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.messagebus.events; package de.srsoftware.umbrella.messagebus.events;
import de.srsoftware.umbrella.core.constants.Field;
import de.srsoftware.umbrella.core.model.Bookmark;
import de.srsoftware.umbrella.core.model.Task;
import de.srsoftware.umbrella.core.model.Translatable;
import de.srsoftware.umbrella.core.model.UmbrellaUser;
import java.util.Collection;
import java.util.List;
import static de.srsoftware.umbrella.core.constants.Module.BOOKMARK; import static de.srsoftware.umbrella.core.constants.Module.BOOKMARK;
import static de.srsoftware.umbrella.core.model.Translatable.t; import static de.srsoftware.umbrella.core.model.Translatable.t;
import de.srsoftware.umbrella.core.constants.Field;
import de.srsoftware.umbrella.core.model.Bookmark;
import de.srsoftware.umbrella.core.model.Translatable;
import de.srsoftware.umbrella.core.model.UmbrellaUser;
import java.util.Collection;
import java.util.List;
public class BookmarkEvent extends Event<Bookmark> { public class BookmarkEvent extends Event<Bookmark> {
public BookmarkEvent(UmbrellaUser initiator, Bookmark bookmark, EventType type){ public BookmarkEvent(UmbrellaUser initiator, Bookmark bookmark, EventType type){
@@ -1,16 +1,15 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.messagebus.events; package de.srsoftware.umbrella.messagebus.events;
import de.srsoftware.umbrella.core.ModuleRegistry;
import de.srsoftware.umbrella.core.api.Owner;
import de.srsoftware.umbrella.core.constants.Field;
import de.srsoftware.umbrella.core.model.*;
import java.util.Collection;
import java.util.List;
import static de.srsoftware.umbrella.core.constants.Field.*; import static de.srsoftware.umbrella.core.constants.Field.*;
import static de.srsoftware.umbrella.core.model.Translatable.t; import static de.srsoftware.umbrella.core.model.Translatable.t;
import de.srsoftware.umbrella.core.ModuleRegistry;
import de.srsoftware.umbrella.core.api.Owner;
import de.srsoftware.umbrella.core.model.*;
import java.util.Collection;
import java.util.List;
public class ItemEvent extends Event<Item>{ public class ItemEvent extends Event<Item>{
public ItemEvent(UmbrellaUser initiator, String module, Item item, EventType type) { public ItemEvent(UmbrellaUser initiator, String module, Item item, EventType type) {
super(initiator, module, item, type); super(initiator, module, item, type);
@@ -37,12 +37,51 @@ import org.json.JSONObject;
public class Util { public class Util {
public static final System.Logger LOG = System.getLogger("Util"); public static final System.Logger LOG = System.getLogger("Util");
private static final Pattern UML_PATTERN = Pattern.compile("@start(\\w+)(.*?)@end(\\1)",Pattern.DOTALL); private static final Pattern UML_PATTERN = Pattern.compile("@start(\\w+)(.*?)@end(\\1)",Pattern.DOTALL);
private static final Pattern SPREADSHEET_PATTERN = Pattern.compile("@startsheet(.*?)@endsheet",Pattern.DOTALL);
private static File plantumlJar = null; private static File plantumlJar = null;
private static final JParsedown MARKDOWN = new JParsedown(); private static final JParsedown MARKDOWN = new JParsedown();
public static final String SHA1 = "SHA-1"; public static final String SHA1 = "SHA-1";
private static final MessageDigest SHA1_DIGEST; private static final MessageDigest SHA1_DIGEST;
private static final Map<Integer,String> umlCache = new HashMap<>(); private static final Map<Integer,String> umlCache = new HashMap<>();
private static final String SCRIPT = """
<script src="http://127.0.0.1:8080/js/jspreadsheet-ce.js"></script>
<div id="spreadsheet"></div>
<script type="application/javascript">
alert('Test');
jspreadsheet(document.getElementById('spreadsheet'), {
worksheets: [
{
data: [
['Jazz', 'Honda', '2019-02-12', '', true, '$ 2.000,00', '#777700'],
['Civic', 'Honda', '2018-07-11', '', true, '$ 4.000,01', '#007777'],
],
columns: [
{ type: 'text', title: 'Car', width: 120 },
{
type: 'dropdown',
title: 'Make',
width: 200,
source: ['Alfa Romeo', 'Audi', 'Bmw', 'Honda'],
},
{ type: 'calendar', title: 'Available', width: 200 },
{ type: 'image', title: 'Photo', width: 120 },
{ type: 'checkbox', title: 'Stock', width: 80 },
{
type: 'numeric',
title: 'Price',
width: 100,
mask: '$ #.##,00',
decimal: ',',
},
{ type: 'color', width: 100, render: 'square' },
],
},
],
});
</script>
""";
static { static {
try { try {
SHA1_DIGEST = MessageDigest.getInstance(SHA1); SHA1_DIGEST = MessageDigest.getInstance(SHA1);
@@ -79,8 +118,22 @@ public class Util {
public static String markdown(String source){ public static String markdown(String source){
if (source == null) return source; if (source == null) return source;
try { try {
var matcher = SPREADSHEET_PATTERN.matcher(source);
var count = 0;
while (matcher.find()){
count++;
var sheetData = matcher.group(0).trim();
var start = matcher.start(0);
var end = matcher.end(0);
source = source.substring(0, start)
+ "<div class=\"spreadsheet\" id=\"spreadsheet-"+count+"\">"
+ sheetData.substring(11,sheetData.length()-10)
+ "</div>"
+ source.substring(end);
matcher = SPREADSHEET_PATTERN.matcher(source);
}
if (plantumlJar != null && plantumlJar.exists()) { if (plantumlJar != null && plantumlJar.exists()) {
var matcher = UML_PATTERN.matcher(source); matcher = UML_PATTERN.matcher(source);
while (matcher.find()) { while (matcher.find()) {
var uml = matcher.group(0).trim(); var uml = matcher.group(0).trim();
var start = matcher.start(0); var start = matcher.start(0);
@@ -150,6 +150,7 @@ public class Field {
public static final String SUBJECT = "subject"; public static final String SUBJECT = "subject";
public static final String TABLE = "table"; public static final String TABLE = "table";
public static final String TAG = "tag";
public static final String TAGS = "tags"; public static final String TAGS = "tags";
public static final String TAG_COLORS = "tag_colors"; public static final String TAG_COLORS = "tag_colors";
public static final String TASK = "task"; public static final String TASK = "task";
@@ -1,19 +1,19 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.core.model; package de.srsoftware.umbrella.core.model;
import static de.srsoftware.umbrella.core.constants.Field.*;
import static java.text.MessageFormat.format;
import de.srsoftware.tools.Mappable; import de.srsoftware.tools.Mappable;
import de.srsoftware.umbrella.core.ModuleRegistry; import de.srsoftware.umbrella.core.ModuleRegistry;
import de.srsoftware.umbrella.core.Util; import de.srsoftware.umbrella.core.Util;
import de.srsoftware.umbrella.core.api.Owner; import de.srsoftware.umbrella.core.api.Owner;
import de.srsoftware.umbrella.core.constants.Field; import de.srsoftware.umbrella.core.constants.Field;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static de.srsoftware.umbrella.core.constants.Field.*;
import static java.text.MessageFormat.format;
public class Poll implements Mappable { public class Poll implements Mappable {
public static class Option implements Mappable{ public static class Option implements Mappable{
+20
View File
@@ -8,6 +8,7 @@
"name": "frontend", "name": "frontend",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"jspreadsheet-ce": "^5.0.4",
"svelte-tiny-router": "^1.0.5" "svelte-tiny-router": "^1.0.5"
}, },
"devDependencies": { "devDependencies": {
@@ -488,6 +489,11 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@jspreadsheet/formula": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@jspreadsheet/formula/-/formula-2.0.2.tgz",
"integrity": "sha512-PDQYf9REQA53I7tVYkvkeyQxrd5jcjUeHgItYnRpjN2QiIQwawSqBDtGGEVQTSboTG+JwgGCuhvOpj7FxeKwew=="
},
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.44.1", "version": "4.44.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.1.tgz",
@@ -951,6 +957,20 @@
"@types/estree": "^1.0.6" "@types/estree": "^1.0.6"
} }
}, },
"node_modules/jspreadsheet-ce": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/jspreadsheet-ce/-/jspreadsheet-ce-5.0.4.tgz",
"integrity": "sha512-ra1JI1n+tEGgRMzTzNkPZjG0HZz8W6bFGAiTiHl+eYarXdRmS5qDc/ua3l2ev7oZ6Og9kjfrXYHVLUWiVc308w==",
"dependencies": {
"@jspreadsheet/formula": "^2.0.2",
"jsuites": "^5.12.0"
}
},
"node_modules/jsuites": {
"version": "5.13.5",
"resolved": "https://registry.npmjs.org/jsuites/-/jsuites-5.13.5.tgz",
"integrity": "sha512-cvkcpy/v5I3+IAcNPE4UP38PFCEfUQw9JI5NN61dlcXLwkD+2UTIOsRPvgMLeqI1eDWHL4AHfrbcE/+TFciUsw=="
},
"node_modules/kleur": { "node_modules/kleur": {
"version": "4.1.5", "version": "4.1.5",
"dev": true, "dev": true,
+1
View File
@@ -14,6 +14,7 @@
"vite": "^6.3.5" "vite": "^6.3.5"
}, },
"dependencies": { "dependencies": {
"jspreadsheet-ce": "^5.0.4",
"svelte-tiny-router": "^1.0.5" "svelte-tiny-router": "^1.0.5"
} }
} }
+2
View File
@@ -33,6 +33,7 @@
import ResetPw from "./routes/user/ResetPw.svelte"; import ResetPw from "./routes/user/ResetPw.svelte";
import Search from "./routes/search/Search.svelte"; import Search from "./routes/search/Search.svelte";
import SendDoc from "./routes/document/Send.svelte"; import SendDoc from "./routes/document/Send.svelte";
import Spreadsheet from "./routes/calc.svelte";
import Stock from './routes/stock/Index.svelte'; import Stock from './routes/stock/Index.svelte';
import TagList from "./routes/tags/Index.svelte"; import TagList from "./routes/tags/Index.svelte";
import TagUses from "./routes/tags/TagUses.svelte"; import TagUses from "./routes/tags/TagUses.svelte";
@@ -89,6 +90,7 @@
<Route path="/" component={User} /> <Route path="/" component={User} />
<Route path="/bookmark" component={Bookmarks} /> <Route path="/bookmark" component={Bookmarks} />
<Route path="/bookmark/:id/view" component={Bookmark} /> <Route path="/bookmark/:id/view" component={Bookmark} />
<Route path="/calc" component={Spreadsheet} />
<Route path="/company" component={Companies} /> <Route path="/company" component={Companies} />
<Route path="/contact" component={ContactList} /> <Route path="/contact" component={ContactList} />
<Route path="/document" component={DocList} /> <Route path="/document" component={DocList} />
+99 -40
View File
@@ -2,59 +2,118 @@
import { t } from '../translations.svelte.js' import { t } from '../translations.svelte.js'
import { tick } from "svelte"; import { tick } from "svelte";
let { let {
getCandidates = async text => { conole.log('no handler for getCandidates('+text+')'); return {};}, autofocus = false,
onSelect = text => [] getCandidates = dummyGetCandidates,
onCommit = dummyOnCommit,
onSelect = dummyOnSelect,
candidate = $bindable({ display : '' })
} = $props(); } = $props();
const ignore = ['Escape','Tab','ArrowUp','ArrowLeft','ArrowRight'] const ignore = ['ArrowLeft','ArrowRight'];
let options = $state({}); //let candidate = $state({ display : '' });
let text = $state('') let selected = $state([]);
let candidates = $derived(getCandidates(candidate.display));
async function dummyGetCandidates(text){
console.warn(`getCandidates(${text}) not overridden!`);
if (!text) return [];
return [
{
display : 'candidate 1',
explanation : 'candidates need to have a display field'
},
{
display : 'candidate 2',
additional : 'other fields are optional',
more : 'and may be domain specific'
},
{ display : text }
];
}
function dummyOnCommit(candidate){
// if Enter is pressed on the input field, this method gets called with
// either the selected candidate or
// an anonymous object with the entered text in the display field
console.warn(`onCommit(${JSON.stringify(candidate)}) not overridden!`);
// if the method returns true, the field will be cleared after committing
return true;
}
function dummyOnSelect(candidate){
console.warn(`${candidate.display} selected, but onSelect not overridden!`)
}
async function ondblclick(evt){ async function ondblclick(evt){
const select = evt.target; const select = evt.target;
const key = select.value; const idx = select.value;
text = options[key]; candidate = candidates[idx];
let result = {}; candidates = [];
result[key] = text; selected = [];
options = {}; console.log(candidate);
text = ''; onSelect(candidate);
onSelect(result);
} }
async function onkeyup(evt){ async function onkeyup(ev){
const select = evt.target; if (ignore.includes(ev.key)) return;
const key = evt.key; if (ev.key == 'ArrowDown'){
if (ignore.includes(key)) return; ev.preventDefault();
if (key == 'ArrowDown'){ selected = selected.length < 1 ? [0] : [selected[0]+1]
if (select.selectedIndex == 0) select.selectedIndex=1; if (selected[0] >= candidates.length) selected = [0];
return; return false;
} }
if (key == 'Enter'){ if (ev.key == 'ArrowUp'){
ondblclick(evt); ev.preventDefault();
return; selected = selected.length < 1 ? [-1] : [selected[0]-1]
if (selected[0] < 0) selected = [candidates.length-1];
return false;
} }
if (key == 'Backspace'){ if (ev.key == 'Enter'|| ev.key == 'Tab'){
text = text.substring(0,text.length-1) ev.preventDefault();
} else if (key.length<2){ if (selected.length>0) {
text += evt.key candidate = candidates[selected[0]];
candidates = [];
selected = [];
onSelect(candidate);
return false;
}
if (ev.key == 'Enter') {
candidates = [];
selected = [];
if (onCommit(candidate)) candidate = { display : '' };
}
return false;
} }
options = await getCandidates(text); if (ev.key == 'Escape'){
await tick(); ev.preventDefault();
for (let o of select.getElementsByTagName('option')) o.selected = false; candidates = [];
selected = [];
return false;
}
candidates = await getCandidates(candidate.display);
if (selected>candidates.length) selected = candidates.length;
return false;
} }
</script> </script>
<style> <style>
select{ span { position : relative }
min-width: 200px; select { position : absolute; top: 30px; left: 3px; }
}
select { background: black; color: orange; border: 1px solid orange; border-radius: 5px; }
option:checked { background: orange; color: black; }
</style> </style>
{#if options}
<select size={Object.keys(options).length<2?2:Object.keys(options).length+1} {onkeyup} {ondblclick} width="40"> <span>
<option>{text}</option> <input type="text" bind:value={candidate.display} {onkeyup} autofocus={autofocus} />
{#each Object.entries(options) as [val,caption]} {#if candidates && candidates.length > 0}
<option value={val}>{caption}</option> <select bind:value={selected} {ondblclick} multiple tabindex="-1">
{/each} {#each candidates as candidate,i}
</select> <option value={i}>{candidate.display}</option>
{/if} {/each}
</select>
{/if}
</span>
@@ -0,0 +1,82 @@
<script>
import { onMount, onDestroy } from 'svelte';
import { t } from '../translations.svelte';
let { classes='markdown', markdown=$bindable({source:'',rendered:''}), onclick = null, oncontextmenu = null, title='', wrapper = 'div' } = $props();
let jspreadsheet = null;
const regex = /@startsheet[\s\S]*?@endsheet/g;
const number = /^[0-9.-]+$/
function update(sheet, index){
const data = sheet.getData(false,false,'|',false);
markdown.source = replaceNthSpreadsheet(markdown.source,index,data);
}
function replaceNthSpreadsheet(text, n, newContent) {
const blocks = text.match(regex) || [];
if (blocks.length < n+1){
console.warn(`cannot replace block ${n}: only ${blocks.length} blocks found!`);
return text;
}
let count = 0;
return text.replace(regex, (match) => count++ === n ? `@startsheet\n${newContent}\n@endsheet` : match);
}
function formatCell(cell, value, x, y, instance, options){
value = value.trim();
if (value.startsWith('=') || number.test(value)) cell.style.textAlign = 'right';
}
async function transform(){
if (!markdown.rendered) return;
let sheets = document.getElementsByClassName('spreadsheet');
for (let i = 0; i < sheets.length; i++) {
let sheet = sheets[i];
let raw = sheet.innerHTML.trim();
if (!jspreadsheet) {
sheet.innerHTML = t('Loading spreadsheet library…');
let module = await import('jspreadsheet-ce'); // path or package name
await import('jspreadsheet-ce/dist/jspreadsheet.css');
jspreadsheet = module.default ?? module;
}
if (!jspreadsheet) break; // break loop if library fails to load
sheet.innerHTML = t('Processing spreadsheet data…');
// Use parseCSV from the helpers
const parsed = jspreadsheet.helpers.parseCSV(raw, '|');
let columns = {};
for (let row of parsed){
for (let col in row){
let data = ""+row[col];
if (data.startsWith('=')) continue;
let len = data.length;
columns[col] = Math.max(columns[col]??0,len);
}
}
columns = Object.values(columns).map((len) => {return {
align: 'left',
render: formatCell,
width:`${len}0px`
}});
let config = {
worksheets : [{
data:parsed,
columns
}],
onchange : (instance, cell, x, y, value) => update(instance, i)
};
let wb = jspreadsheet(document.getElementById(sheet.id), config);
}
}
onMount(() => { setTimeout(transform,200)});
</script>
{#if markdown.rendered}
<svelte:element this={wrapper} class={classes} {onclick} {oncontextmenu} {title}>
{@html markdown.rendered}
</svelte:element>
{/if}
@@ -3,6 +3,8 @@
import { api, target } from '../urls.svelte.js'; import { api, target } from '../urls.svelte.js';
import { t } from '../translations.svelte.js'; import { t } from '../translations.svelte.js';
import Display from './MarkdownDisplay.svelte';
let { let {
editable = true, editable = true,
onclick = evt => {}, onclick = evt => {},
@@ -136,7 +138,7 @@
<span id="restore_markdown" onclick={restore} class="hint">{t('unsaved_content')}</span> <span id="restore_markdown" onclick={restore} class="hint">{t('unsaved_content')}</span>
{/if} {/if}
<textarea bind:value={editValue.source} onkeyup={typed} autofocus={!simple}></textarea> <textarea bind:value={editValue.source} onkeyup={typed} autofocus={!simple}></textarea>
<div class="preview">{@html target(editValue.rendered)}</div> <Display classes="preview" bind:markdown={editValue} />
{#if !simple} {#if !simple}
<div class="buttons"> <div class="buttons">
<button class="cancel" onclick={e => editing = false}>{t('cancel')}</button> <button class="cancel" onclick={e => editing = false}>{t('cancel')}</button>
@@ -144,6 +146,6 @@
</div> </div>
{/if} {/if}
{:else} {:else}
<svelte:element this={type} {onclick} {oncontextmenu} class={{editable}} title={t('right_click_to_edit')} >{@html target(value.rendered)}</svelte:element> <Display classes={{editable}} markdown={value} {onclick} {oncontextmenu} title={t('right_click_to_edit')} wrapper={type} />
{/if} {/if}
</div> </div>
@@ -1,8 +1,9 @@
<script> <script>
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { api } from '../urls.svelte'; import { api, get, post } from '../urls.svelte';
import { t } from '../translations.svelte'; import { t } from '../translations.svelte';
import { error, yikes } from '../warn.svelte';
import Autocomplete from './Autocomplete.svelte'; import Autocomplete from './Autocomplete.svelte';
import PermissionSelector from './PermissionSelector.svelte'; import PermissionSelector from './PermissionSelector.svelte';
@@ -27,7 +28,7 @@
}); });
if (resp.ok){ if (resp.ok){
var json = await resp.json(); var json = await resp.json();
return Object.fromEntries(Object.values(json).map(user => [user.id,user.name])); return Object.values(json).map(user => { return {...user,display:user.name}; });
} else { } else {
return []; return [];
} }
@@ -35,16 +36,10 @@
async function loadPermissions(){ async function loadPermissions(){
const url = api('task/permissions'); const url = api('task/permissions');
const resp = await fetch(url,{credentials: 'include'}); const resp = await get(url);
if (resp.ok){ if (resp.ok){
permissions = await resp.json(); permissions = await resp.json();
} else { } else error(resp);
message = await resp.text();
}
}
function onSelect(entry){
addMember(entry);
} }
onMount(loadPermissions); onMount(loadPermissions);
@@ -66,7 +61,7 @@
<tr> <tr>
<td>{t('add_object',{object:t('member')})}</td> <td>{t('add_object',{object:t('member')})}</td>
<td> <td>
<Autocomplete {getCandidates} {onSelect} /> <Autocomplete {getCandidates} onSelect={addMember} />
</td> </td>
</tr> </tr>
</tbody> </tbody>
+1 -3
View File
@@ -16,9 +16,7 @@
} }
function onSelect(entry){ function onSelect(entry){
for (let [k,v] of Object.entries(entry)){ users[entry.id] = entry;
users[k] = {name:v,id:k};
}
} }
let sortedUsers = $derived.by(() => Object.values(users).sort((a, b) => a.name.localeCompare(b.name))); let sortedUsers = $derived.by(() => Object.values(users).sort((a, b) => a.name.localeCompare(b.name)));
+1 -3
View File
@@ -37,9 +37,7 @@
if (resp.ok){ if (resp.ok){
yikes(); yikes();
const input = await resp.json(); const input = await resp.json();
return Object.fromEntries( return Object.values(input).map(user => {return {...user,display:user.name}});
Object.entries(input).map(([key, value]) => [key, value.name])
);
} else { } else {
error(resp); error(resp);
return {}; return {};
+59
View File
@@ -0,0 +1,59 @@
<script>
import { onMount } from 'svelte';
var spreadsheet = null;
const config = {
worksheets: [{
data: [
["1","Sum of A:","=SUM(A1:A99)"],
["2"],
["3"],
["4"]
],
columns: [
{ type: 'autonumber', title: 'amount' },
{ type: 'text', width: '350px', title: 'description', align: 'right' },
{ type: 'text', width: '250px', title: 'value' },
],
// Name of the worksheet
worksheetName: 'Albums'
}],
onchange: update
};
function update(instance, cell, x, y, value) {
console.log({instance,cell,x,y,value});
console.log(spreadsheet[0].getData());
}
let loading = true;
let module = null;
async function load(){
try {
const module = await import('jspreadsheet-ce'); // path or package name
await import('jspreadsheet-ce/dist/jspreadsheet.css');
let jspreadsheet = module.default ?? module;
let element = document.getElementById('spreadsheet');
console.log(element);
spreadsheet = jspreadsheet(element, config);
} catch (e) {
console.log(e);
} finally {
loading = false;
}
}
onMount(load);
</script>
{#if loading}
Loading…
{:else}
{/if}
<div id="spreadsheet">Spreadsheet loading…</div>
+1 -3
View File
@@ -22,9 +22,7 @@
if (resp.ok){ if (resp.ok){
yikes(); yikes();
const input = await resp.json(); const input = await resp.json();
return Object.fromEntries( return Object.values(input).map(user => { return {...user, display: user.name}});
Object.entries(input).map(([key, value]) => [key, value.name])
);
} else { } else {
error(resp); error(resp);
return {}; return {};
+1 -1
View File
@@ -20,7 +20,7 @@
let members = $state([]); let members = $state([]);
function addMember(member){ function addMember(member){
for (let [id,name] of Object.entries(member)) update_permissions({user_id:+id,permission:4}); update_permissions({user_id:+member.id,permission:4});
return true; return true;
} }
+25 -9
View File
@@ -2,11 +2,12 @@
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import { useTinyRouter } from 'svelte-tiny-router'; import { useTinyRouter } from 'svelte-tiny-router';
import { api, patch, post, eventStream, target } from '../../urls.svelte.js'; import { api, get, patch, post, eventStream, target } from '../../urls.svelte.js';
import { error, messages, yikes } from '../../warn.svelte'; import { error, messages, yikes } from '../../warn.svelte';
import { t } from '../../translations.svelte.js'; import { t } from '../../translations.svelte.js';
import { user } from '../../user.svelte.js'; import { user } from '../../user.svelte.js';
import Autocomplete from '../../Components/Autocomplete.svelte';
import Card from './KanbanCard.svelte'; import Card from './KanbanCard.svelte';
import LineEditor from '../../Components/LineEditor.svelte'; import LineEditor from '../../Components/LineEditor.svelte';
import MarkdownEditor from '../../Components/MarkdownEditor.svelte'; import MarkdownEditor from '../../Components/MarkdownEditor.svelte';
@@ -16,12 +17,12 @@
let connectionStatus = 'disconnected'; let connectionStatus = 'disconnected';
let { id } = $props(); let { id } = $props();
let descr = $state(false); let descr = $state(false);
let filter_input = $state(''); let filter_input = $state({display:''});
let router = useTinyRouter(); let router = useTinyRouter();
if (router.hasQueryParam('filter')) filter_input = router.getQueryParam('filter'); if (router.hasQueryParam('filter')) filter_input = {display:router.getQueryParam('filter')};
let dragged = null; let dragged = null;
let highlight = $state({}); let highlight = $state({});
let filter = $derived(filter_input.toLowerCase()); let filter = $derived(filter_input.display.toLowerCase());
let project = $state(null); let project = $state(null);
let tasks = $state({}); let tasks = $state({});
let users = []; let users = [];
@@ -29,7 +30,7 @@
let info = $state(null); let info = $state(null);
let task_form = $state(false); let task_form = $state(false);
let stateList = {}; let stateList = {};
$effect(() => updateUrl(filter_input)); $effect(() => updateUrl(filter_input.display));
function byName(a,b) { function byName(a,b) {
return a.name.localeCompare(b.name); return a.name.localeCompare(b.name);
@@ -92,6 +93,17 @@
} }
} }
async function getCandidates(input){
if (!input || input.length <3) return [];
const url = api(`tags/search/${encodeURI(input)}`);
const res = await get(url);
if (res.ok){
yikes();
const list = await res.json();
return list.map(elem => {return {display:elem}});
} else error(res);
}
function handleCreateEvent(evt){ function handleCreateEvent(evt){
handleEvent(evt,'create'); handleEvent(evt,'create');
} }
@@ -191,6 +203,10 @@
} }
} }
function onCommit(){
return false;
}
function openTask(task_id){ function openTask(task_id){
window.open(`/task/${task_id}/view`, '_blank').focus(); window.open(`/task/${task_id}/view`, '_blank').focus();
} }
@@ -219,8 +235,8 @@
const user_ids = Object.values(project.members).map(member => member.user.id); const user_ids = Object.values(project.members).map(member => member.user.id);
const data = { const data = {
url : location.href, url : location.href,
tags : ['Kanban', project.name, filter_input], tags : ['Kanban', project.name, filter_input.display],
comment : `${project.name}: ${filter_input}`, comment : `${project.name}: ${filter_input.display}`,
share : user_ids share : user_ids
} }
const url = api('bookmark'); const url = api('bookmark');
@@ -239,7 +255,7 @@
function updateUrl(){ function updateUrl(){
let url = window.location.origin + window.location.pathname; let url = window.location.origin + window.location.pathname;
if (filter_input) url += '?filter=' + encodeURI(filter_input); if (filter_input.display) url += '?filter=' + encodeURI(filter_input.display);
window.history.replaceState(window.history.state, '', url); window.history.replaceState(window.history.state, '', url);
} }
@@ -272,7 +288,7 @@
</fieldset> </fieldset>
<div class="kanban" style="display: grid; grid-template-columns: {`repeat(${columns}, auto)`}"> <div class="kanban" style="display: grid; grid-template-columns: {`repeat(${columns}, auto)`}">
<span class="filter"> <span class="filter">
<input type="text" bind:value={filter_input} autofocus /> <Autocomplete {getCandidates} bind:candidate={filter_input} {onCommit} autofocus={true} />
{t('filter')} {t('filter')}
<button style="visibility:{filter_input ? 'visible' : 'hidden'}" onclick={save_bookmark}> <button style="visibility:{filter_input ? 'visible' : 'hidden'}" onclick={save_bookmark}>
<span class="symbol"></span> {t('save_object',{object:t('bookmark')})} <span class="symbol"></span> {t('save_object',{object:t('bookmark')})}
+3 -19
View File
@@ -28,9 +28,8 @@
let new_state = $state({code:null,name:null}) let new_state = $state({code:null,name:null})
let state_available=$derived(new_state.name && new_state.code && !project.allowed_states[new_state.code]); let state_available=$derived(new_state.name && new_state.code && !project.allowed_states[new_state.code]);
async function addMember(entry){ async function addMember(user){
const ids = Object.keys(entry); return await update({new_member:+user.id});
if (ids) update({new_member:+ids.pop()});
} }
async function addState(){ async function addState(){
@@ -67,21 +66,6 @@
update({drop_member:member.user.id}); update({drop_member:member.user.id});
} }
async function getCandidates(text){
const url = api('user/search');
const resp = await fetch(url,{
credentials : 'include',
method : 'POST',
body : text
});
if (resp.ok){
var json = await resp.json();
return Object.fromEntries(Object.values(json).map(user => [user.id,user.name]));
} else {
return [];
}
}
function handleCreate(evt){ function handleCreate(evt){
let json = JSON.parse(evt.data); let json = JSON.parse(evt.data);
json.event = 'create'; json.event = 'create';
@@ -242,7 +226,7 @@
</label> </label>
<div class="em">{t('members')}</div> <div class="em">{t('members')}</div>
<div class="em"> <div class="em">
<PermissionEditor members={project.members} {updatePermission} {addMember} {dropMember} {getCandidates} /> <PermissionEditor members={project.members} {updatePermission} {addMember} {dropMember} />
</div> </div>
{#if project.allowed_states} {#if project.allowed_states}
{#each Object.keys(project.allowed_states) as key,idx} {#each Object.keys(project.allowed_states) as key,idx}
+37
View File
@@ -0,0 +1,37 @@
<script>
import { onMount } from 'svelte';
import { useTinyRouter } from 'svelte-tiny-router';
import { api, get } from '../../urls.svelte';
import { error, yikes } from '../../warn.svelte';
import { t } from '../../translations.svelte';
let {module, id} = $props();
let item = $state(null);
let router = useTinyRouter();
function onclick(e){
e.preventDefault();
var target = e.target;
while (target && !target.href) target=target.parentNode;
let href = target.getAttribute('href');
if (href) router.navigate(href);
return false;
}
async function loadItem(){
let url = api(`stock/item/${id}`);
let res = await get(url);
if (res.ok) {
yikes();
item = await res.json();
} else error();
}
onMount(loadItem);
</script>
{#if item}
<a href="/stock/{id}/view" {onclick} >({item.code}) {item.name}</a>
{/if}
+5 -1
View File
@@ -6,6 +6,8 @@
import { error, yikes } from '../../warn.svelte'; import { error, yikes } from '../../warn.svelte';
import { t } from '../../translations.svelte.js'; import { t } from '../../translations.svelte.js';
import ItemDisplay from '../stock/display.svelte';
let { module, id } = $props(); let { module, id } = $props();
let object = $state(null); let object = $state(null);
let router = useTinyRouter(); let router = useTinyRouter();
@@ -44,7 +46,9 @@
{:else if module=='wiki'} {:else if module=='wiki'}
<span onclick={go}>{object.title}</span> <span onclick={go}>{object.title}</span>
{:else if module=='document'} {:else if module=='document'}
<span onclick={go}>{t(object.type)} ${object.number} (${object.customer.name.split('\n')[0]})</span> <span onclick={go}>{t('type_'+object.type)} {object.number} ({object.customer.name.split('\n')[0]})</span>
{:else if module=='stock'}
<ItemDisplay {module} {id} />
{:else} {:else}
<span class="error">No display defined in Reference.svelte for entities of type {module}.</span> <span class="error">No display defined in Reference.svelte for entities of type {module}.</span>
{/if} {/if}
+23 -8
View File
@@ -2,7 +2,9 @@
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.js'; import Autocomplete from '../../Components/Autocomplete.svelte';
import { api, get, post } from '../../urls.svelte.js';
import { error, yikes } from '../../warn.svelte'; import { error, yikes } from '../../warn.svelte';
import { t } from '../../translations.svelte.js'; import { t } from '../../translations.svelte.js';
import { user } from '../../user.svelte.js' import { user } from '../../user.svelte.js'
@@ -13,10 +15,9 @@
tags = $bindable([]), tags = $bindable([]),
user_list = [], user_list = [],
} = $props(); } = $props();
let newTag = $state('');
let router = useTinyRouter(); let router = useTinyRouter();
async function addTag(){ async function addTag(newTag){
if (!newTag) return; if (!newTag) return;
if (!id) { if (!id) {
// when creating elements, they don`t have an id, yet // when creating elements, they don`t have an id, yet
@@ -63,10 +64,21 @@
return false; return false;
} }
async function getCandidates(input){
if (!input || input.length <3) return [];
const url = api(`tags/search/${encodeURI(input)}`);
const res = await get(url);
if (res.ok){
yikes();
const list = await res.json();
return list.map(elem => {return {display:elem}});
} else error(res);
}
async function loadTags(entityId){ async function loadTags(entityId){
if (!entityId) return; // when crating elements, they dont`t have an id, yet. if (!entityId) return; // when crating elements, they dont`t have an id, yet.
const url = api(`tags/${module}/${entityId}`); const url = api(`tags/${module}/${entityId}`);
const resp = await fetch(url,{credentials:'include'}); const resp = await get(url);
if (resp.ok) { if (resp.ok) {
tags = await resp.json(); tags = await resp.json();
tags = tags.sort(); tags = tags.sort();
@@ -75,13 +87,16 @@
} }
} }
async function onCommit(wrapped){
addTag(wrapped.display);
}
function onSelect(dummy){}
function show(tag){ function show(tag){
router.navigate(`/tags/use/${tag}`); router.navigate(`/tags/use/${tag}`);
} }
function typed(ev){
if (ev.keyCode == 13) addTag();
}
$effect(() => loadTags(id)); $effect(() => loadTags(id));
</script> </script>
@@ -93,6 +108,6 @@
</span> </span>
{/each} {/each}
<span class="tag editor"> <span class="tag editor">
<input type="text" bind:value={newTag} onkeyup={typed} /> <Autocomplete {getCandidates} {onCommit} {onSelect} />
</span> </span>
</div> </div>
+7 -1
View File
@@ -12,6 +12,12 @@
let router = useTinyRouter(); let router = useTinyRouter();
let uses = $state(null); let uses = $state(null);
function headline(module){
if (module == 'stock') return t(module);
if (module.endsWith('s')) return t(`${module}s`);
return t(module);
}
async function loadUses(){ async function loadUses(){
const url = api(`tags/uses/${tag}`); const url = api(`tags/uses/${tag}`);
const resp = await fetch(url,{credentials:'include'}); const resp = await fetch(url,{credentials:'include'});
@@ -36,7 +42,7 @@
</legend> </legend>
{#if uses} {#if uses}
{#each Object.entries(uses) as [module,ids]} {#each Object.entries(uses) as [module,ids]}
<h2>{t(module.endsWith('s') ? module : `${module}s`)}</h2> <h2>{headline(module)}</h2>
<ul> <ul>
{#each ids as id} {#each ids as id}
<li><Reference {module} {id} /></li> <li><Reference {module} {id} /></li>
+4 -4
View File
@@ -29,7 +29,8 @@
let router = useTinyRouter(); let router = useTinyRouter();
function addMember(member){ function addMember(member){
for (let uid of Object.keys(member)) task.members[uid] = project.members[uid]; let uid = member.id;
task.members[uid] = project.members[uid];
} }
function flat(json){ function flat(json){
@@ -44,10 +45,9 @@
async function getCandidates(text){ async function getCandidates(text){
const origin = parent_task ? parent_task.members : project.members; const origin = parent_task ? parent_task.members : project.members;
const candidates = Object.values(origin) return Object.values(origin)
.filter(member => member.user.name.toLowerCase().includes(text.toLowerCase())) .filter(member => member.user.name.toLowerCase().includes(text.toLowerCase()))
.map(member => [member.user.id,member.user.name]); .map(member => {return { ...member.user,display:member.user.name}});
return Object.fromEntries(candidates);
} }
async function load(){ async function load(){
+7 -6
View File
@@ -37,9 +37,8 @@
router.navigate(`/task/${id}/add_subtask`); router.navigate(`/task/${id}/add_subtask`);
} }
async function addMember(entry){ async function addMember(newMember){
const ids = Object.keys(entry); return await update({new_member:+newMember.id});
if (ids) update({new_member:+ids.pop()});
} }
async function addTime(){ async function addTime(){
@@ -64,10 +63,10 @@
async function getCandidates(text){ async function getCandidates(text){
const origin = task.parent ? task.parent.members : project.members; const origin = task.parent ? task.parent.members : project.members;
const candidates = Object.values(origin) return Object.values(origin)
.filter(member => member.user.name.toLowerCase().includes(text.toLowerCase())) .filter(member => member.user.name.toLowerCase().includes(text.toLowerCase()))
.map(member => [member.user.id,member.user.name]); .map(member => { return {...member.user,display:member.user.name}});
return Object.fromEntries(candidates);
} }
function gotoKanban(){ function gotoKanban(){
@@ -177,6 +176,8 @@
}); });
if (resp.ok){ if (resp.ok){
yikes(); yikes();
let json = await resp.json();
if (json.members) task.members = json.members;
return true; return true;
} else { } else {
error(resp); error(resp);
+3 -4
View File
@@ -22,9 +22,8 @@
async function addMember(entry){ async function addMember(entry){
let newMembers = JSON.parse(JSON.stringify(page.members)); let newMembers = JSON.parse(JSON.stringify(page.members));
for (var id of Object.keys(entry)){ let id = entry.id;
if (!newMembers[id]) newMembers[id] = { permission : {name:'READ_ONLY'} }; if (!newMembers[id]) newMembers[id] = { permission : {name:'READ_ONLY'} };
}
return patch({members:newMembers}); return patch({members:newMembers});
} }
@@ -74,7 +73,7 @@
}); });
if (resp.ok){ if (resp.ok){
var json = await resp.json(); var json = await resp.json();
return Object.fromEntries(Object.values(json).filter(nonMember).map(user => [user.id,user.name])); return Object.values(json).filter(nonMember).map(user => { return {...user,display:user.name}});
} else { } else {
return []; return [];
} }
@@ -4,7 +4,6 @@ package de.srsoftware.umbrella.poll;
import de.srsoftware.umbrella.core.model.Permission; import de.srsoftware.umbrella.core.model.Permission;
import de.srsoftware.umbrella.core.model.Poll; import de.srsoftware.umbrella.core.model.Poll;
import de.srsoftware.umbrella.core.model.UmbrellaUser; import de.srsoftware.umbrella.core.model.UmbrellaUser;
import java.util.Collection; import java.util.Collection;
import java.util.Map; import java.util.Map;
@@ -11,7 +11,7 @@ import static de.srsoftware.umbrella.poll.Constants.CONFIG_DATABASE;
import static java.lang.System.Logger.Level.WARNING; import static java.lang.System.Logger.Level.WARNING;
import static java.net.HttpURLConnection.HTTP_OK; import static java.net.HttpURLConnection.HTTP_OK;
import static java.text.MessageFormat.format; import static java.text.MessageFormat.format;
import static de.srsoftware.umbrella.core.model.Permission.READ_ONLY;
import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpExchange;
import de.srsoftware.configuration.Configuration; import de.srsoftware.configuration.Configuration;
import de.srsoftware.tools.Path; import de.srsoftware.tools.Path;
@@ -26,10 +26,9 @@ import de.srsoftware.umbrella.core.model.Permission;
import de.srsoftware.umbrella.core.model.Poll; import de.srsoftware.umbrella.core.model.Poll;
import de.srsoftware.umbrella.core.model.Token; import de.srsoftware.umbrella.core.model.Token;
import de.srsoftware.umbrella.core.model.UmbrellaUser; import de.srsoftware.umbrella.core.model.UmbrellaUser;
import org.json.JSONObject;
import java.io.IOException; import java.io.IOException;
import java.util.*; import java.util.*;
import org.json.JSONObject;
public class PollModule extends BaseHandler implements PollService { public class PollModule extends BaseHandler implements PollService {
@@ -21,7 +21,6 @@ import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.core.model.Permission; import de.srsoftware.umbrella.core.model.Permission;
import de.srsoftware.umbrella.core.model.Poll; import de.srsoftware.umbrella.core.model.Poll;
import de.srsoftware.umbrella.core.model.UmbrellaUser; import de.srsoftware.umbrella.core.model.UmbrellaUser;
import java.sql.Connection; import java.sql.Connection;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.*; import java.util.*;
@@ -10,6 +10,7 @@ import static de.srsoftware.tools.jdbc.Query.SelectQuery.ALL;
import static de.srsoftware.umbrella.bookmarks.Constants.*; import static de.srsoftware.umbrella.bookmarks.Constants.*;
import static de.srsoftware.umbrella.core.Errors.*; import static de.srsoftware.umbrella.core.Errors.*;
import static de.srsoftware.umbrella.core.constants.Field.*; import static de.srsoftware.umbrella.core.constants.Field.*;
import static de.srsoftware.umbrella.core.constants.Field.TAG;
import static de.srsoftware.umbrella.core.constants.Field.TAGS; import static de.srsoftware.umbrella.core.constants.Field.TAGS;
import static de.srsoftware.umbrella.core.constants.Module.BOOKMARK; import static de.srsoftware.umbrella.core.constants.Module.BOOKMARK;
import static de.srsoftware.umbrella.core.constants.Text.TABLE_WITH_NAME; import static de.srsoftware.umbrella.core.constants.Text.TABLE_WITH_NAME;
@@ -25,8 +26,10 @@ import de.srsoftware.tools.Tuple;
import de.srsoftware.tools.jdbc.Query; import de.srsoftware.tools.jdbc.Query;
import de.srsoftware.umbrella.bookmarks.BookmarkDb; import de.srsoftware.umbrella.bookmarks.BookmarkDb;
import de.srsoftware.umbrella.core.BaseDb; import de.srsoftware.umbrella.core.BaseDb;
import de.srsoftware.umbrella.core.constants.Field;
import de.srsoftware.umbrella.core.constants.Text; import de.srsoftware.umbrella.core.constants.Text;
import de.srsoftware.umbrella.core.model.Translatable; import de.srsoftware.umbrella.core.model.Translatable;
import de.srsoftware.umbrella.core.model.UmbrellaUser;
import java.sql.Connection; import java.sql.Connection;
import java.sql.SQLException; import java.sql.SQLException;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -333,6 +336,19 @@ CREATE TABLE IF NOT EXISTS {0} (
} }
} }
@Override
public Collection<String> search(String key, UmbrellaUser user) {
try {
var tags = new HashSet<String>();
var rs = select("DISTINCT tag").from(TABLE_TAGS).where(USER_ID,equal(user.id())).where(Field.TAG,like("%"+key+"%")).exec(db);
while (rs.next()) tags.add(rs.getString(1));
rs.close();
return tags;
} catch (SQLException s){
throw failedToLoadObject(Text.TAGS);
}
}
@Override @Override
public void updateId(String module, Object oldId, Object newId) { public void updateId(String module, Object oldId, Object newId) {
try { try {
@@ -3,6 +3,7 @@ package de.srsoftware.umbrella.tags;
import de.srsoftware.tools.Tuple; import de.srsoftware.tools.Tuple;
import de.srsoftware.umbrella.core.model.UmbrellaUser;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -32,5 +33,7 @@ public interface TagDB {
void save(Collection<Long> userIds, String module, long entityId, Collection<String> tags); void save(Collection<Long> userIds, String module, long entityId, Collection<String> tags);
Collection<String> search(String key, UmbrellaUser user);
void updateId(String module, Object oldId, Object newId); void updateId(String module, Object oldId, Object newId);
} }
@@ -5,6 +5,7 @@ import static de.srsoftware.umbrella.core.ConnectionProvider.connect;
import static de.srsoftware.umbrella.core.ModuleRegistry.userService; import static de.srsoftware.umbrella.core.ModuleRegistry.userService;
import static de.srsoftware.umbrella.core.ResponseCode.HTTP_UNPROCESSABLE; import static de.srsoftware.umbrella.core.ResponseCode.HTTP_UNPROCESSABLE;
import static de.srsoftware.umbrella.core.constants.Field.USER_LIST; import static de.srsoftware.umbrella.core.constants.Field.USER_LIST;
import static de.srsoftware.umbrella.core.constants.Path.SEARCH;
import static de.srsoftware.umbrella.core.constants.Path.USES; import static de.srsoftware.umbrella.core.constants.Path.USES;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*; import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*;
import static de.srsoftware.umbrella.tags.Constants.*; import static de.srsoftware.umbrella.tags.Constants.*;
@@ -66,11 +67,13 @@ public class TagModule extends BaseHandler implements TagService {
var user = userService().refreshSession(ex); var user = userService().refreshSession(ex);
if (user.isEmpty()) return unauthorized(ex); if (user.isEmpty()) return unauthorized(ex);
var module = path.pop(); var module = path.pop();
if (module == null) return getUserTags(ex, user.get()); return switch (module){
var head = path.pop(); case SEARCH -> searchTags(ex,path.pop(),user.get());
if (USES.equals(module)) return getTagUses(ex,head,user.get()); case USES -> getTagUses(ex,path.pop(),user.get());
long entityId = Long.parseLong(head); case null -> getUserTags(ex, user.get());
return sendContent(ex, getTags(module,entityId,user.get())); default -> sendContent(ex, getTags(module,Long.parseLong(path.pop()),user.get()));
};
} catch (NumberFormatException e){ } catch (NumberFormatException e){
return sendContent(ex,HTTP_UNPROCESSABLE,"Entity id missing in path."); return sendContent(ex,HTTP_UNPROCESSABLE,"Entity id missing in path.");
} catch (UmbrellaException e){ } catch (UmbrellaException e){
@@ -150,6 +153,10 @@ public class TagModule extends BaseHandler implements TagService {
return tag; return tag;
} }
private boolean searchTags(HttpExchange ex, String head, UmbrellaUser user) throws IOException {
return sendContent(ex, tagDb.search(head, user));
}
@Override @Override
public void updateId(String module, Object oldId, Object newId) { public void updateId(String module, Object oldId, Object newId) {
tagDb.updateId(module,oldId,newId); tagDb.updateId(module,oldId,newId);
@@ -32,7 +32,6 @@ import de.srsoftware.tools.SessionToken;
import de.srsoftware.umbrella.core.BaseHandler; import de.srsoftware.umbrella.core.BaseHandler;
import de.srsoftware.umbrella.core.ModuleRegistry; import de.srsoftware.umbrella.core.ModuleRegistry;
import de.srsoftware.umbrella.core.api.*; import de.srsoftware.umbrella.core.api.*;
import de.srsoftware.umbrella.core.constants.Field;
import de.srsoftware.umbrella.core.constants.Text; import de.srsoftware.umbrella.core.constants.Text;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.core.model.*; import de.srsoftware.umbrella.core.model.*;
+17
View File
@@ -11,3 +11,20 @@ tasks.processResources {
} }
duplicatesStrategy = DuplicatesStrategy.EXCLUDE duplicatesStrategy = DuplicatesStrategy.EXCLUDE
} }
fun download(url : String, destination : String){
var destFile = projectDir.toPath().resolve(destination).toFile();
destFile.parentFile.mkdirs()
if (!destFile.exists()) {
System.out.println("Downloading "+url)
ant.invokeMethod("get", mapOf("src" to url, "dest" to destFile))
}
}
tasks.register("downloadLib"){
download("https://bossanova.uk/jspreadsheet/v5/jspreadsheet.js", "src/main/resources/web/js/jspreadsheet-ce.js")
}
tasks.named("compileJava") {
dependsOn("downloadLib")
}
@@ -317,6 +317,23 @@ tr:hover .taglist .tag button {
border: 1px solid red; border: 1px solid red;
} }
.jss_worksheet{
background: black !important;
border-right: 1px solid #333 !important;
border-bottom: 1px solid #333 !important;
}
.jss_worksheet > thead > tr > td,
.jss_worksheet > tbody > tr > td:first-child{
background: #730000 !important;
color: yellow;
}
.jss_worksheet td{
border-top: 1px solid #333 !important;
border-left: 1px solid #333 !important;
}
@media screen and (max-width: 900px) { @media screen and (max-width: 900px) {
#app nav a{ #app nav a{
background: black; background: black;
@@ -308,6 +308,23 @@ tr:hover .taglist .tag button {
background: #a00; background: #a00;
} }
.jss_worksheet{
background: black !important;
border-right: 1px solid #333 !important;
border-bottom: 1px solid #333 !important;
}
.jss_worksheet > thead > tr > td,
.jss_worksheet > tbody > tr > td:first-child{
background: orange !important;
color: black;
}
.jss_worksheet td{
border-top: 1px solid #333 !important;
border-left: 1px solid #333 !important;
}
@media screen and (max-width: 900px) { @media screen and (max-width: 900px) {
#app nav a{ #app nav a{
background: black; background: black;
File diff suppressed because one or more lines are too long