Browse Source

fine-tuning notes in preparation for release

Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
feature/brute_force_protection
Stephan Richter 3 months ago
parent
commit
85ae67b72f
  1. 4
      core/src/main/java/de/srsoftware/umbrella/core/model/Note.java
  2. 2
      frontend/src/App.svelte
  3. 2
      frontend/src/routes/company/Index.svelte
  4. 2
      frontend/src/routes/document/View.svelte
  5. 83
      frontend/src/routes/notes/Index.svelte
  6. 73
      frontend/src/routes/notes/List.svelte
  7. 99
      frontend/src/routes/notes/RelatedNotes.svelte
  8. 2
      frontend/src/routes/project/View.svelte
  9. 2
      frontend/src/routes/task/View.svelte
  10. 49
      notes/src/main/java/de/srsoftware/umbrella/notes/NoteModule.java
  11. 2
      notes/src/main/java/de/srsoftware/umbrella/notes/NotesDb.java
  12. 5
      notes/src/main/java/de/srsoftware/umbrella/notes/SqliteDb.java

4
core/src/main/java/de/srsoftware/umbrella/core/model/Note.java

@ -11,12 +11,12 @@ import java.sql.SQLException;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Map; import java.util.Map;
public record Note(long id, String module, long entityId, long authorId, String text, LocalDateTime timestamp) implements Mappable { public record Note(long id, String module, String entityId, long authorId, String text, LocalDateTime timestamp) implements Mappable {
public static Note of(ResultSet rs) throws SQLException { public static Note of(ResultSet rs) throws SQLException {
return new Note( return new Note(
rs.getLong(ID), rs.getLong(ID),
rs.getString(MODULE), rs.getString(MODULE),
rs.getLong(ENTITY_ID), rs.getString(ENTITY_ID),
rs.getLong(USER_ID), rs.getLong(USER_ID),
rs.getString(NOTE), rs.getString(NOTE),
LocalDateTime.ofEpochSecond(rs.getLong(TIMESTAMP),0, UTC) LocalDateTime.ofEpochSecond(rs.getLong(TIMESTAMP),0, UTC)

2
frontend/src/App.svelte

@ -19,7 +19,7 @@
import Login from "./Components/Login.svelte"; import Login from "./Components/Login.svelte";
import Messages from "./routes/message/Messages.svelte"; import Messages from "./routes/message/Messages.svelte";
import Menu from "./Components/Menu.svelte"; import Menu from "./Components/Menu.svelte";
import Notes from "./routes/notes/List.svelte"; import Notes from "./routes/notes/Index.svelte";
import ProjectList from "./routes/project/List.svelte"; import ProjectList from "./routes/project/List.svelte";
import ProjectAdd from "./routes/project/Create.svelte"; import ProjectAdd from "./routes/project/Create.svelte";
import ResetPw from "./routes/user/ResetPw.svelte"; import ResetPw from "./routes/user/ResetPw.svelte";

2
frontend/src/routes/company/Index.svelte

@ -5,7 +5,7 @@
import Editor from './Editor.svelte'; import Editor from './Editor.svelte';
import LineEditor from '../../Components/LineEditor.svelte'; import LineEditor from '../../Components/LineEditor.svelte';
import Notes from '../notes/List.svelte'; import Notes from '../notes/RelatedNotes.svelte';
let error = $state(null); let error = $state(null);
let companies = $state(null); let companies = $state(null);

2
frontend/src/routes/document/View.svelte

@ -9,7 +9,7 @@
import LineEditor from '../../Components/LineEditor.svelte'; import LineEditor from '../../Components/LineEditor.svelte';
import MarkdownEditor from '../../Components/MarkdownEditor.svelte'; import MarkdownEditor from '../../Components/MarkdownEditor.svelte';
import MultilineEditor from '../../Components/MultilineEditor.svelte'; import MultilineEditor from '../../Components/MultilineEditor.svelte';
import Notes from '../notes/List.svelte'; import Notes from '../notes/RelatedNotes.svelte';
import PositionList from './PositionList.svelte'; import PositionList from './PositionList.svelte';
import PositionSelector from './PositionSelector.svelte'; import PositionSelector from './PositionSelector.svelte';
import StateSelector from './StateSelector.svelte'; import StateSelector from './StateSelector.svelte';

83
frontend/src/routes/notes/Index.svelte

@ -0,0 +1,83 @@
<script>
import { onMount } from 'svelte';
import { useTinyRouter } from 'svelte-tiny-router';
import { api } from '../../urls.svelte.js';
import { t } from '../../translations.svelte.js';
import { user } from '../../user.svelte.js';
import List from './List.svelte';
let authors = $state({});
let error = $state(null);
let loader = {
offset : 0,
limit : 5,
active : true
}
let note = $state({source:null,rendered:null});
let notes = $state(null);
let {
module = null,
entity_id = null
} = $props();
async function loadNotes(){
const url = api(`notes?offset=${loader.offset}&limit=${loader.limit}`);
const resp = await fetch(url,{credentials:'include'});
if (resp.ok){
const data = await resp.json();
if (!notes) notes = [];
notes.push(...Object.values(data.notes).sort((a, b) => b.id - a.id));
authors = {...authors, ...data.authors};
loader.offset += loader.limit;
loader.active = false;
error = null;
if (Object.keys(data.notes).length) onscroll(null); // when notes were received, check whether they fill up the page
} else {
error = await resp.text();
}
}
async function saveNote(){
const url = api(`notes/${module}/${entity_id}`);
const resp = await fetch(url,{
credentials : 'include',
method : 'POST',
body : note.source
});
if (resp.ok){
let newNote = await resp.json();
authors[user.id] = user;
notes[newNote.id] = newNote;
note = {source:'',rendered:''};
error = null;
return true;
} else {
error = await resp.text();
return false;
}
}
function onscroll(ev){
if (window.innerHeight + window.scrollY >= document.body.offsetHeight && !loader.active) {
loader.active = true;
loadNotes();
}
}
loadNotes(loadNotes)
</script>
<svelte:window {onscroll} />
{#if error}
<span class="error">{error}</span>
{/if}
<List {notes} />

73
frontend/src/routes/notes/List.svelte

@ -1,5 +1,4 @@
<script> <script>
import { onMount } from 'svelte';
import { useTinyRouter } from 'svelte-tiny-router'; import { useTinyRouter } from 'svelte-tiny-router';
import { api } from '../../urls.svelte.js'; import { api } from '../../urls.svelte.js';
@ -8,15 +7,9 @@
import Editor from '../../Components/MarkdownEditor.svelte'; import Editor from '../../Components/MarkdownEditor.svelte';
let authors = $state(null); let { authors, module, notes = $bindable() } = $props();
let error = $state(null); let error = $state(null);
let note = $state({source:null,rendered:null});
let notes = $state(null);
const router = useTinyRouter(); const router = useTinyRouter();
let {
module = null,
entity_id = null
} = $props();
async function drop(nid){ async function drop(nid){
if (!confirm(t('confirm_delete',{element:t('note')}))) return; if (!confirm(t('confirm_delete',{element:t('note')}))) return;
@ -27,46 +20,22 @@
}); });
if (resp.ok) { if (resp.ok) {
error = false; error = false;
delete notes[nid]; notes = notes.filter((itm,idx) => itm.id != nid);
} else { } else {
error = await resp.text(); error = await resp.text();
} }
} }
function goToEntity(n){ function goToEntity(note){
router.navigate(`/${n.module}/${n.entity_id}/view`); router.navigate(`/${note.module}/${note.entity_id}/view`);
} }
async function load(){ function title(note){
const url = (module == null && entity_id == null) ? api('notes') : api(`notes/${module}/${entity_id}`); let title = t(note.module);
const resp = await fetch(url,{credentials:'include'}); if (note.module == 'wiki') title += ':';
if (resp.ok){ title += ' ';
const data = await resp.json(); title += note.entity_id;
notes = data.notes; return title;
authors = data.authors;
} else {
error = await resp.text();
}
}
async function saveNote(){
const url = api(`notes/${module}/${entity_id}`);
const resp = await fetch(url,{
credentials : 'include',
method : 'POST',
body : note.source
});
if (resp.ok){
let newNote = await resp.json();
authors[user.id] = user;
notes[newNote.id] = newNote;
note = {source:'',rendered:''};
error = null;
return true;
} else {
error = await resp.text();
return false;
}
} }
async function update(nid,src){ async function update(nid,src){
@ -81,34 +50,26 @@
return true; return true;
} else { } else {
error = await resp.text(); error = await resp.text();
return false;
} }
} }
onMount(load)
</script> </script>
{#if error}
<span class="error">{error}</span>
{/if}
{#if notes} {#if notes}
{#each Object.entries(notes) as [nid,note]} {#each notes as note (note.id)}
<fieldset> <fieldset>
{#if module} {#if module}
<legend class="author">{authors[note.user_id].name}</legend> <legend class="author">{authors[note.user_id].name}</legend>
{:else} {:else}
<legend class="entity" onclick={() => goToEntity(note)}>{t(note.module)} {note.entity_id}</legend> <legend class="entity" onclick={() => goToEntity(note)}>{title(note)}</legend>
{/if} {/if}
<legend class="time"> <legend class="time">
{note.timestamp.replace('T',' ')} {note.timestamp.replace('T',' ')}
{#if user.id == note.user_id} {#if user.id == note.user_id}
<button class="symbol" onclick={() => drop(nid)}></button> <button class="symbol" onclick={() => drop(note.id)}></button>
{/if} {/if}
</legend> </legend>
<Editor value={note.text} onSet={(newVal) => update(nid,newVal)} editable={user.id == note.user_id} /> <Editor value={note.text} onSet={(newVal) => update(note.id,newVal)} editable={user.id == note.user_id} />
</fieldset> </fieldset>
{/each} {/each}
{/if} {/if}
<div class="editor">
<Editor simple={true} bind:value={note} onSet={saveNote} />
<button onclick={saveNote}>{t('save_object',{object:t('note')})}</button>
</div>

99
frontend/src/routes/notes/RelatedNotes.svelte

@ -0,0 +1,99 @@
<script>
import { onMount } from 'svelte';
import { useTinyRouter } from 'svelte-tiny-router';
import { api } from '../../urls.svelte.js';
import { t } from '../../translations.svelte.js';
import { user } from '../../user.svelte.js';
import Editor from '../../Components/MarkdownEditor.svelte';
import List from './List.svelte';
let authors = $state(null);
let error = $state(null);
let note = $state({source:null,rendered:null});
let notes = $state(null);
const router = useTinyRouter();
let {
module = null,
entity_id = null
} = $props();
async function drop(nid){
if (!confirm(t('confirm_delete',{element:t('note')}))) return;
const url = api(`notes/${nid}`);
const resp = await fetch(url,{
credentials : 'include',
method : 'DELETE'
});
if (resp.ok) {
error = false;
delete notes[nid];
} else {
error = await resp.text();
}
}
function goToEntity(n){
router.navigate(`/${n.module}/${n.entity_id}/view`);
}
async function load(){
const url = api(`notes/${module}/${entity_id}`);
const resp = await fetch(url,{credentials:'include'});
if (resp.ok){
const data = await resp.json();
notes = Object.values(data.notes).sort((a, b) => a.id - b.id);
authors = data.authors;
} else {
error = await resp.text();
}
}
async function saveNote(){
const url = api(`notes/${module}/${entity_id}`);
const resp = await fetch(url,{
credentials : 'include',
method : 'POST',
body : note.source
});
if (resp.ok){
let newNote = await resp.json();
authors[user.id] = user;
notes.push(newNote);
note = {source:'',rendered:''};
error = null;
return true;
} else {
error = await resp.text();
return false;
}
}
async function update(nid,src){
const url = api(`notes/${nid}`);
const resp = await fetch(url,{
credentials : 'include',
method : 'PATCH',
body : src
});
if (resp.ok) {
error = false;
return true;
} else {
error = await resp.text();
return true;
}
}
onMount(load);
</script>
{#if error}
<span class="error">{error}</span>
{/if}
<List {authors} {module} {notes} />
<div class="editor">
<Editor simple={true} bind:value={note} onSet={saveNote} />
<button onclick={saveNote}>{t('save_object',{object:t('note')})}</button>
</div>

2
frontend/src/routes/project/View.svelte

@ -8,7 +8,7 @@
import LineEditor from '../../Components/LineEditor.svelte'; import LineEditor from '../../Components/LineEditor.svelte';
import MarkdownEditor from '../../Components/MarkdownEditor.svelte'; import MarkdownEditor from '../../Components/MarkdownEditor.svelte';
import PermissionEditor from '../../Components/PermissionEditor.svelte'; import PermissionEditor from '../../Components/PermissionEditor.svelte';
import Notes from '../notes/List.svelte'; import Notes from '../notes/RelatedNotes.svelte';
import StateSelector from '../../Components/StateSelector.svelte'; import StateSelector from '../../Components/StateSelector.svelte';
import Tags from '../tags/TagList.svelte'; import Tags from '../tags/TagList.svelte';
import TaskList from '../task/TaskList.svelte'; import TaskList from '../task/TaskList.svelte';

2
frontend/src/routes/task/View.svelte

@ -8,7 +8,7 @@
import LineEditor from '../../Components/LineEditor.svelte'; import LineEditor from '../../Components/LineEditor.svelte';
import MarkdownEditor from '../../Components/MarkdownEditor.svelte'; import MarkdownEditor from '../../Components/MarkdownEditor.svelte';
import PermissionEditor from '../../Components/PermissionEditor.svelte'; import PermissionEditor from '../../Components/PermissionEditor.svelte';
import Notes from '../notes/List.svelte'; import Notes from '../notes/RelatedNotes.svelte';
import StateSelector from '../../Components/StateSelector.svelte'; import StateSelector from '../../Components/StateSelector.svelte';
import TagList from '../tags/TagList.svelte'; import TagList from '../tags/TagList.svelte';
import TaskList from './TaskList.svelte'; import TaskList from './TaskList.svelte';

49
notes/src/main/java/de/srsoftware/umbrella/notes/NoteModule.java

@ -2,6 +2,8 @@
package de.srsoftware.umbrella.notes; package de.srsoftware.umbrella.notes;
import static de.srsoftware.umbrella.core.ConnectionProvider.connect; import static de.srsoftware.umbrella.core.ConnectionProvider.connect;
import static de.srsoftware.umbrella.core.Constants.LIMIT;
import static de.srsoftware.umbrella.core.Constants.OFFSET;
import static de.srsoftware.umbrella.core.ResponseCode.HTTP_UNPROCESSABLE; import static de.srsoftware.umbrella.core.ResponseCode.HTTP_UNPROCESSABLE;
import static de.srsoftware.umbrella.core.Util.mapValues; import static de.srsoftware.umbrella.core.Util.mapValues;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*; import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*;
@ -74,15 +76,10 @@ public class NoteModule extends BaseHandler implements NoteService {
var user = registry.userService().refreshSession(ex); var user = registry.userService().refreshSession(ex);
if (user.isEmpty()) return unauthorized(ex); if (user.isEmpty()) return unauthorized(ex);
var module = path.pop(); var module = path.pop();
Map<Long,Note> notes = null; return switch (module){
if (module == null) { case null -> sendContent(ex,getUserNotes(ex,user.get()));
notes = notesDb.list(user.get().id()); default -> sendContent(ex,getEntityNotes(module,path.pop()));
} else { };
var head = path.pop();
notes = getNotes(module, head);
}
var authors = notes.values().stream().map(Note::authorId).distinct().map(registry.userService()::loadUser).collect(Collectors.toMap(UmbrellaUser::id,UmbrellaUser::toMap));
return sendContent(ex, Map.of("notes",mapValues(notes),"authors",authors));
} 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){
@ -90,6 +87,28 @@ public class NoteModule extends BaseHandler implements NoteService {
} }
} }
private boolean getUserNotes(HttpExchange ex, UmbrellaUser user) throws IOException {
var param = queryParam(ex);
long offset = switch (param.get(OFFSET)){
case String s -> Long.parseLong(s);
case Number n -> n.longValue();
case null, default -> 0;
};
long limit = switch (param.get(LIMIT)){
case String s -> Long.parseLong(s);
case Number n -> n.longValue();
case null, default -> 100;
};
var notes = notesDb.list(user.id(),offset,limit);
return sendContent(ex,addUsers(notes));
}
private Map<String, Object> addUsers(Map<Long, Note> notes) {
var authors = notes.values().stream().map(Note::authorId).distinct().map(registry.userService()::loadUser).collect(Collectors.toMap(UmbrellaUser::id,UmbrellaUser::toMap));
return Map.of("notes",mapValues(notes),"authors",authors);
}
@Override @Override
public boolean doPatch(Path path, HttpExchange ex) throws IOException { public boolean doPatch(Path path, HttpExchange ex) throws IOException {
addCors(ex); addCors(ex);
@ -122,8 +141,7 @@ public class NoteModule extends BaseHandler implements NoteService {
if (user.isEmpty()) return unauthorized(ex); if (user.isEmpty()) return unauthorized(ex);
var module = path.pop(); var module = path.pop();
if (module == null) throw unprocessable("Module missing in path."); if (module == null) throw unprocessable("Module missing in path.");
var head = path.pop(); var entityId = path.pop();
long entityId = Long.parseLong(head);
String text = body(ex); String text = body(ex);
if (text.isBlank()) throw missingFieldException("Note text"); if (text.isBlank()) throw missingFieldException("Note text");
var note = new Note(0,module,entityId,user.get().id(),text, LocalDateTime.now()); var note = new Note(0,module,entityId,user.get().id(),text, LocalDateTime.now());
@ -136,8 +154,13 @@ public class NoteModule extends BaseHandler implements NoteService {
} }
} }
public Map<Long,Note> getNotes(String module, String entityId) throws UmbrellaException{ public Map<String,Object> getEntityNotes(String module, String entityId) throws UmbrellaException{
return notesDb.list(module,entityId); return addUsers(getNotes(module, entityId));
}
@Override
public Map<Long, Note> getNotes(String module, String entityId) throws UmbrellaException {
return notesDb.list(module, entityId);
} }
@Override @Override

2
notes/src/main/java/de/srsoftware/umbrella/notes/NotesDb.java

@ -13,7 +13,7 @@ public interface NotesDb {
* get all lists of a person * get all lists of a person
* @return * @return
*/ */
Map<Long, Note> list(long authorId); Map<Long, Note> list(long authorId, long offset, long limit);
/** /**
* get the notes related to a specific entity * get the notes related to a specific entity

5
notes/src/main/java/de/srsoftware/umbrella/notes/SqliteDb.java

@ -167,10 +167,11 @@ CREATE TABLE IF NOT EXISTS "{0}" (
} }
@Override @Override
public Map<Long, Note> list(long authorId) { public Map<Long, Note> list(long authorId, long offset, long limit) {
try { try {
var notes = new HashMap<Long, Note>(); var notes = new HashMap<Long, Note>();
var rs = select(ALL).from(TABLE_NOTES).where(USER_ID,equal(authorId)).exec(db); var rs = select(ALL).from(TABLE_NOTES).where(USER_ID,equal(authorId))
.sort(format("{0} DESC",ID)).skip(offset).limit(limit).exec(db);
while (rs.next()) { while (rs.next()) {
var note = Note.of(rs); var note = Note.of(rs);
notes.put(note.id(),note); notes.put(note.id(),note);

Loading…
Cancel
Save