Compare commits

...

11 Commits

Author SHA1 Message Date
StephanRichter 8392edf408 Merge branch 'main' into module/journal 2026-04-22 08:26:07 +02:00
StephanRichter 9c80e0d77c preparing for journal with timestamps
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-04-22 08:24:54 +02:00
StephanRichter 0429e14715 Merge branch 'bugfix/tag-header'
Build Docker Image / Docker-Build (push) Successful in 2m35s
Build Docker Image / Clean-Registry (push) Successful in 6s
2026-04-20 08:21:26 +02:00
StephanRichter ff618f75fa css work
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-04-19 11:07:47 +02:00
StephanRichter dbfd50bfa9 html improvement
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-04-19 10:40:03 +02:00
StephanRichter ff65cfd958 bugfix: now trimming tags on creation
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-04-18 15:36:35 +02:00
StephanRichter 877547df0d bugfix: now trimming tags on creation
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-04-18 15:28:21 +02:00
StephanRichter 0db8ed4867 added header for tags in wiki pages
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-04-18 15:07:54 +02:00
StephanRichter 55ece851be added title to wiki pages
Build Docker Image / Docker-Build (push) Successful in 1m59s
Build Docker Image / Clean-Registry (push) Successful in -15s
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-04-09 09:12:12 +02:00
StephanRichter ed26f6e46f Merge branch 'bugfix/wiki-css'
Build Docker Image / Docker-Build (push) Successful in 2m6s
Build Docker Image / Clean-Registry (push) Successful in -11s
2026-03-31 00:16:37 +02:00
StephanRichter c04dfe225c improved spreadsheet editing
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-03-30 23:59:00 +02:00
10 changed files with 79 additions and 28 deletions
+27 -8
View File
@@ -2,7 +2,15 @@
import { onMount, onDestroy } from 'svelte'; import { onMount, onDestroy } from 'svelte';
import { t } from '../translations.svelte'; import { t } from '../translations.svelte';
let { classes='markdown', markdown=$bindable({source:'',rendered:''}), onclick = null, oncontextmenu = null, title='', wrapper = 'div' } = $props(); let {
classes='markdown',
markdown=$bindable({source:'',rendered:''}),
onclick = null,
oncontextmenu = e => {},
sheet = null,
title='',
wrapper = 'div'
} = $props();
let jspreadsheet = null; let jspreadsheet = null;
const regex = /@startsheet[\s\S]*?@endsheet/g; const regex = /@startsheet[\s\S]*?@endsheet/g;
const number = /^[0-9.-]+$/ const number = /^[0-9.-]+$/
@@ -31,16 +39,16 @@
if (!markdown.rendered) return; if (!markdown.rendered) return;
let sheets = document.getElementsByClassName('spreadsheet'); let sheets = document.getElementsByClassName('spreadsheet');
for (let i = 0; i < sheets.length; i++) { for (let i = 0; i < sheets.length; i++) {
let sheet = sheets[i]; let current_sheet = sheets[i];
let raw = sheet.innerHTML.trim(); let raw = current_sheet.innerHTML.trim();
if (!jspreadsheet) { if (!jspreadsheet) {
sheet.innerHTML = t('Loading spreadsheet library…'); current_sheet.innerHTML = t('Loading spreadsheet library…');
let module = await import('jspreadsheet-ce'); // path or package name let module = await import('jspreadsheet-ce'); // path or package name
await import('jspreadsheet-ce/dist/jspreadsheet.css'); await import('jspreadsheet-ce/dist/jspreadsheet.css');
jspreadsheet = module.default ?? module; jspreadsheet = module.default ?? module;
} }
if (!jspreadsheet) break; // break loop if library fails to load if (!jspreadsheet) break; // break loop if library fails to load
sheet.innerHTML = t('Processing spreadsheet data…'); current_sheet.innerHTML = t('Processing spreadsheet data…');
// Use parseCSV from the helpers // Use parseCSV from the helpers
@@ -60,14 +68,25 @@
render: formatCell, render: formatCell,
width:`${len}0px` width:`${len}0px`
}}); }});
var w = window.innerWidth;
if (classes == 'preview') w = w/2;
let config = { let config = {
worksheets : [{ worksheets : [{
data:parsed, data:parsed,
columns columns,
tableOverflow: true,
tableWidth: `${w}px`,
}], }],
onchange : (instance, cell, x, y, value) => update(instance, i) onchange : (instance, cell, x, y, value) => update(instance, i),
oneditionstart : (instance, cell, x, y) => oncontextmenu({sheet:current_sheet.id, x,y})
}; };
let wb = jspreadsheet(document.getElementById(sheet.id), config); let wb = jspreadsheet(document.getElementById(current_sheet.id), config);
if (sheet && sheet.sheet == current_sheet.id) {
let cell = wb[0].getCellFromCoords(sheet.x, sheet.y);
cell.scrollIntoView({block:'center'});
wb[0].updateSelectionFromCoords(sheet.x, sheet.y);
wb[0].openEditor(cell);
}
} }
} }
+10 -2
View File
@@ -20,6 +20,7 @@
let start = 0; let start = 0;
let stored_source = $state(store_id ? localStorage.getItem(store_id) : null); let stored_source = $state(store_id ? localStorage.getItem(store_id) : null);
let timer = null; let timer = null;
let sheet = null;
async function applyEdit(){ async function applyEdit(){
let success = await onSet(editValue.source); let success = await onSet(editValue.source);
@@ -80,8 +81,11 @@
function oncontextmenu(evt){ function oncontextmenu(evt){
if (evt.target) {
evt.preventDefault(); evt.preventDefault();
evt.stopPropagation(); evt.stopPropagation();
}
sheet = evt.sheet ? evt : null; // store position of activated cell to focus after editing starts
startEdit(); startEdit();
return false; return false;
} }
@@ -96,6 +100,10 @@
measured(evt, evt.timeStamp - start); measured(evt, evt.timeStamp - start);
} }
function onresize(evt){
console.log('onresize()',evt);
}
function ontouchstart(evt){ function ontouchstart(evt){
evt.preventDefault(); evt.preventDefault();
start = evt.timeStamp; start = evt.timeStamp;
@@ -137,8 +145,8 @@
{#if stored_source} {#if stored_source}
<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} onresize={onresize} data="test" autofocus={!simple}></textarea>
<Display classes="preview" bind:markdown={editValue} /> <Display classes="preview" bind:markdown={editValue} sheet={sheet} />
{#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>
+3 -3
View File
@@ -241,7 +241,7 @@
</script> </script>
<h2>{t('Stock')}</h2> <h2>{t('Stock')}</h2>
<div class="grid3"> <div class="stock grid3">
<div class="locations"> <div class="locations">
{#if top_level} {#if top_level}
{#each top_level as realm,idx} {#each top_level as realm,idx}
@@ -278,11 +278,11 @@
</div> </div>
{#if item && data && data.users} {#if item && data && data.users}
<div class="tags"> <div class="tags">
<span>{t('tags')}</span> <h4>{t('tags')}</h4>
<Tags module="stock" id={item.id} user_list={data.users} /> <Tags module="stock" id={item.id} user_list={data.users} />
</div> </div>
<div class="notes"> <div class="notes">
<span>{t('notes')}</span> <h4>{t('notes')}</h4>
<Notes module="stock" entity_id={item.id} /> <Notes module="stock" entity_id={item.id} />
</div> </div>
{/if} {/if}
+1 -1
View File
@@ -88,7 +88,7 @@
} }
async function onCommit(wrapped){ async function onCommit(wrapped){
addTag(wrapped.display); addTag(wrapped.display.trim());
} }
function onSelect(dummy){} function onSelect(dummy){}
+8
View File
@@ -150,6 +150,11 @@
$effect(loadPage); $effect(loadPage);
onMount(connectToBus); onMount(connectToBus);
</script> </script>
<svelte:head>
<title>Umbrella {t('wiki')}: {page?.title}</title>
</svelte:head>
{#if page && page.versions} {#if page && page.versions}
<div class="wiki page"> <div class="wiki page">
<div class="versions"> <div class="versions">
@@ -193,7 +198,10 @@
{/if} {/if}
{/if} {/if}
<MarkdownEditor {editable} value={page.content} onSet={s => patch({content:s})} store_id="wiki/{page.id}/description" /> <MarkdownEditor {editable} value={page.content} onSet={s => patch({content:s})} store_id="wiki/{page.id}/description" />
<div class="tags">
<h3>{t('tags')}</h3>
<TagList module="wiki" id={page.id} user_list={Object.keys(page.members).map(id => +id)} /> <TagList module="wiki" id={page.id} user_list={Object.keys(page.members).map(id => +id)} />
</div>
<div class="notes"> <div class="notes">
<h3>{t('notes')}</h3> <h3>{t('notes')}</h3>
<Notes module="wiki" entity_id={page.id} /> <Notes module="wiki" entity_id={page.id} />
@@ -13,6 +13,8 @@ import de.srsoftware.umbrella.core.BaseDb;
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.ZoneOffset;
public class SqliteDb extends BaseDb implements JournalDb{ public class SqliteDb extends BaseDb implements JournalDb{
public SqliteDb(Connection connection) { public SqliteDb(Connection connection) {
@@ -33,13 +35,14 @@ public class SqliteDb extends BaseDb implements JournalDb{
var sql = """ var sql = """
CREATE TABLE IF NOT EXISTS {0} ( CREATE TABLE IF NOT EXISTS {0} (
{1} INTEGER PRIMARY KEY, {1} INTEGER PRIMARY KEY,
{2} INTEGER, {2} LONG NOT NULL,
{3} VARCHAR(255) NOT NULL, {3} INTEGER,
{4} VARCHAR(16) NOT NULL, {4} VARCHAR(255) NOT NULL,
{5} TEXT {5} VARCHAR(16) NOT NULL,
{6} TEXT
); );
"""; """;
sql = format(sql,TABLE_JOURNAL,ID,USER_ID,MODULE,ACTION,DESCRIPTION); sql = format(sql,TABLE_JOURNAL,ID,TIMESTAMP,USER_ID,MODULE,ACTION,DESCRIPTION);
try { try {
db.prepareStatement(sql).execute(); db.prepareStatement(sql).execute();
} catch (SQLException e) { } catch (SQLException e) {
@@ -50,8 +53,9 @@ public class SqliteDb extends BaseDb implements JournalDb{
@Override @Override
public void logEvent(Event<?> event) { public void logEvent(Event<?> event) {
try { try {
insertInto(TABLE_JOURNAL,USER_ID,MODULE,ACTION,DESCRIPTION) var timestamp = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC);
.values(event.initiator().id(), event.module(), event.eventType(), event.describe()) insertInto(TABLE_JOURNAL,TIMESTAMP,USER_ID,MODULE,ACTION,DESCRIPTION)
.values(timestamp,event.initiator().id(), event.module(), event.eventType(), event.describe())
.execute(db).close(); .execute(db).close();
} catch (SQLException e) { } catch (SQLException e) {
throw databaseException(ERROR_WRITE_EVENT,event.eventType(),event.initiator().name()); throw databaseException(ERROR_WRITE_EVENT,event.eventType(),event.initiator().name());
@@ -144,7 +144,7 @@ public class TagModule extends BaseHandler implements TagService {
@Override @Override
public void save(String module, long entityId, Collection<Long> userIds, Collection<String> tags) { public void save(String module, long entityId, Collection<Long> userIds, Collection<String> tags) {
tagDb.save(userIds,module,entityId,tags); tagDb.save(userIds,module,entityId,tags.stream().map(String::trim).toList());
} }
@Override @Override
@@ -334,6 +334,10 @@ tr:hover .taglist .tag button {
border-left: 1px solid #333 !important; border-left: 1px solid #333 !important;
} }
.jss_worksheet tr:hover td input{
color: yellow;
}
@media screen and (max-width: 900px) { @media screen and (max-width: 900px) {
#app nav a{ #app nav a{
background: black; background: black;
@@ -325,6 +325,10 @@ tr:hover .taglist .tag button {
border-left: 1px solid #333 !important; border-left: 1px solid #333 !important;
} }
.jss_worksheet tr:hover td input{
color: black;
}
@media screen and (max-width: 900px) { @media screen and (max-width: 900px) {
#app nav a{ #app nav a{
background: black; background: black;
@@ -524,6 +524,10 @@ a.wikilink{
grid-template-columns: [left] 1fr [first] 1fr [second] 1fr [right] grid-template-columns: [left] 1fr [first] 1fr [second] 1fr [right]
} }
.stock.grid3{
grid-template-columns: [left] 1fr [first] 1fr [second] 2fr [right]
}
.grid3 .locations { .grid3 .locations {
grid-row-end: span 3; grid-row-end: span 3;
} }