working on wiki module: adding users+permissions, adding notes, adding tags

adding tag currently does not work due to the tag module not allowing for strings as entity ids
This commit is contained in:
2025-09-11 20:21:34 +02:00
parent aad9a7d9b8
commit a81eb4cb1b
5 changed files with 160 additions and 30 deletions

View File

@@ -1,7 +1,7 @@
package de.srsoftware.umbrella.core.model; package de.srsoftware.umbrella.core.model;
import de.srsoftware.tools.Mappable; import de.srsoftware.tools.Mappable;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import de.srsoftware.umbrella.core.api.UserService;
import org.json.JSONObject; import org.json.JSONObject;
import java.sql.ResultSet; import java.sql.ResultSet;
@@ -10,6 +10,7 @@ import java.util.*;
import static de.srsoftware.umbrella.core.Constants.*; import static de.srsoftware.umbrella.core.Constants.*;
import static de.srsoftware.umbrella.core.Util.markdown; import static de.srsoftware.umbrella.core.Util.markdown;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.invalidFieldException;
public class WikiPage implements Mappable { public class WikiPage implements Mappable {
@@ -30,10 +31,33 @@ public class WikiPage implements Mappable {
return content; return content;
} }
private WikiPage content(String newVal) {
if (content.equals(newVal)) return this;
content = newVal;
version++;
dirtyFields.add(CONTENT);
return this;
}
public Set<String> dirtyFields(){
return Set.copyOf(dirtyFields);
}
public String id(){ public String id(){
return id; return id;
} }
private WikiPage id(String newVal) {
if (id.equals(newVal)) return this;
id = newVal;
dirtyFields.add(ID);
return this;
}
public boolean isDirty(String field) {
return dirtyFields.contains(field);
}
public Map<Long,Member> members(){ public Map<Long,Member> members(){
return members; return members;
} }
@@ -42,23 +66,41 @@ public class WikiPage implements Mappable {
return new WikiPage(rs.getString(ID),rs.getInt(VERSION),rs.getString(CONTENT)); return new WikiPage(rs.getString(ID),rs.getInt(VERSION),rs.getString(CONTENT));
} }
public WikiPage patch(JSONObject json) { public WikiPage patch(JSONObject json, UserService users) {
for (var key : json.keySet()){ for (var key : json.keySet()){
var val = json.get(key); var val = json.get(key);
var altered = true;
switch (key){ switch (key){
case ID: case ID:
if (!(val instanceof String s)) throw UmbrellaException.invalidFieldException(ID,"String"); if (!(val instanceof String s)) throw invalidFieldException(ID,"String");
id = s; break; id(s);
break;
case CONTENT: case CONTENT:
if (!(val instanceof String s)) throw UmbrellaException.invalidFieldException(CONTENT,"String"); if (!(val instanceof String s)) throw invalidFieldException(CONTENT,"String");
content = s; break; content(s);
default: break;
altered = false; case MEMBERS:
if (!(val instanceof JSONObject membersJson)) throw invalidFieldException(MEMBERS,"Json");
for (var uid : membersJson.keySet()){
var userId = Long.parseLong(uid);
var user = users.loadUser(userId);
if (!(membersJson.get(uid) instanceof JSONObject memberJSON)) throw invalidFieldException(MEMBERS+"."+uid,"Json");
var p = memberJSON.get(PERMISSION);
if (p == JSONObject.NULL){
members.remove(userId);
dirtyFields.add(MEMBERS);
break;
} }
if (altered) { if (!(p instanceof JSONObject perm)) throw invalidFieldException(String.join(".",MEMBERS,uid,PERMISSION),"Json");
dirtyFields.add(key); if (!(perm.get(NAME) instanceof String permName)) throw invalidFieldException(String.join(".",MEMBERS,uid,PERMISSION,NAME),"String");
version++; var permission = Permission.valueOf(permName);
var member = new Member(user,permission);
var existing = members.get(userId);
if (existing == null || existing.permission() != permission) {
members.put(userId, member);
dirtyFields.add(MEMBERS);
}
}
break;
} }
} }
return this; return this;

View File

@@ -170,7 +170,7 @@
<ul> <ul>
{#each Object.values(tasks) as task} {#each Object.values(tasks) as task}
<li> <li>
<a href="/task/{task.id}/view" {onclick} >{task.name}</a> <a href="/task/{task.id}/view" {onclick} class={task.status.name}>{task.name}</a>
</li> </li>
{/each} {/each}
</ul> </ul>

View File

@@ -3,23 +3,63 @@
import { useTinyRouter } from 'svelte-tiny-router'; import { useTinyRouter } from 'svelte-tiny-router';
import { api } from '../../urls.svelte'; import { api } from '../../urls.svelte';
import { t } from '../../translations.svelte'; import { t } from '../../translations.svelte';
import { user } from '../../user.svelte';
import Editor from '../../Components/MarkdownEditor.svelte'; import Editor from '../../Components/MarkdownEditor.svelte';
import Notes from '../notes/RelatedNotes.svelte';
import PermissionEditor from '../../Components/PermissionEditor.svelte';
import TagList from '../tags/TagList.svelte';
let error = $state(null); let error = $state(null);
let { id, version } = $props(); let { id, version } = $props();
let page = $state(null); let page = $state(null);
let router = useTinyRouter(); let router = useTinyRouter();
let members = $state({});
async function addMember(entry){
let newMembers = JSON.parse(JSON.stringify(page.members));
for (var id of Object.keys(entry)){
if (!newMembers[id]) newMembers[id] = { permission : {name:'READ_ONLY'} };
}
return patch({members:newMembers});
}
async function dropMember(member){
var id = member.user.id;
let newMembers = JSON.parse(JSON.stringify(page.members));
newMembers[id].permission = null;
return patch({members:newMembers});
}
function nonMember(json){
return !page.members[json.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).filter(nonMember).map(user => [user.id,user.name]));
} else {
return [];
}
}
async function loadContent(res){ async function loadContent(res){
if (res.ok){ if (res.ok){
page = null; page = null;
page = await res.json(); page = await res.json();
page.versions.sort((a,b)=>b-a); page.versions.sort((a,b)=>b-a);
version = page.version;
error = null; error = null;
return true;
} else { } else {
error = await res.text(); error = await res.text();
return false;
} }
} }
@@ -45,7 +85,13 @@
method:'PATCH', method:'PATCH',
body:JSON.stringify(data) body:JSON.stringify(data)
}); });
loadContent(res); return loadContent(res);
}
async function updatePermission(uid, newPerm){
let newMembers = JSON.parse(JSON.stringify(page.members));
newMembers[uid] = {permission:newPerm};
return patch({members:newMembers});
} }
$effect(loadPage); $effect(loadPage);
@@ -57,15 +103,38 @@
<span class="version">{t('version')}</span> <span class="version">{t('version')}</span>
{#each page.versions as v} {#each page.versions as v}
<span class="version"> <span class="version">
<a href={`/wiki/${id}/version/${v}`} {onclick} class={page.version == v?'selected':''}>{v}</a> <a href={`/wiki/${id}/version/${v}`} {onclick} class={page.version == v?'selected':''}>{v}</a> &nbsp;
&nbsp;
</span> </span>
{/each} {/each}
{/if}
<h2>{id}</h2> <h2>{id}</h2>
{#if page}
{#if page.version != page.versions[0]} {#if page.version != page.versions[0]}
<span class="warn">{t('not_recent_version')}</span> <span class="warn">{t('not_recent_version')}</span>
{/if} {/if}
{#if page.members[user.id].permission.code<4}
<Editor editable={true} value={page.content} onSet={s => patch({content:s})}></Editor> <Editor editable={true} value={page.content} onSet={s => patch({content:s})}></Editor>
<PermissionEditor members={page.members} {addMember} {dropMember} {getCandidates} {updatePermission} />
{:else}
<div class="markdown">{@html page.content.rendered}</div>
<table>
<thead>
<tr>
<th>{t('user')}</th>
<th>{t('permissions')}</th>
</tr>
</thead>
<tbody>
{#each Object.values(page.members) as member,id}
<tr>
<td>{member.user.name}</td>
<td>{t('permission_'+member.permission.name.toLowerCase())}</td>
</tr>
{/each}
</tbody>
</table>
{/if}
<TagList module="wiki" {id} user_list={Object.keys(page.members).map(id => +id)} />
<div class="notes">
<h3>{t('notes')}</h3>
<Notes module="wiki" entity_id={id} />
</div>
{/if} {/if}

View File

@@ -1,6 +1,7 @@
package de.srsoftware.umbrella.wiki; package de.srsoftware.umbrella.wiki;
import de.srsoftware.tools.jdbc.Condition; import de.srsoftware.tools.jdbc.Condition;
import de.srsoftware.tools.jdbc.Query;
import de.srsoftware.umbrella.core.BaseDb; import de.srsoftware.umbrella.core.BaseDb;
import de.srsoftware.umbrella.core.model.Permission; import de.srsoftware.umbrella.core.model.Permission;
import de.srsoftware.umbrella.core.model.WikiPage; import de.srsoftware.umbrella.core.model.WikiPage;
@@ -9,13 +10,14 @@ import java.sql.Connection;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.*; import java.util.*;
import static de.srsoftware.tools.jdbc.Query.*;
import static de.srsoftware.tools.jdbc.Query.SelectQuery.ALL; import static de.srsoftware.tools.jdbc.Query.SelectQuery.ALL;
import static de.srsoftware.tools.jdbc.Query.insertInto;
import static de.srsoftware.tools.jdbc.Query.select;
import static de.srsoftware.umbrella.core.Constants.*; import static de.srsoftware.umbrella.core.Constants.*;
import static de.srsoftware.umbrella.core.Constants.ERROR_FAILED_CREATE_TABLE; import static de.srsoftware.umbrella.core.Constants.ERROR_FAILED_CREATE_TABLE;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.databaseException; import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.databaseException;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.notFound; import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.notFound;
import static de.srsoftware.umbrella.core.model.Permission.EDIT;
import static de.srsoftware.umbrella.core.model.Permission.READ_ONLY;
import static de.srsoftware.umbrella.wiki.Constants.*; import static de.srsoftware.umbrella.wiki.Constants.*;
import static java.lang.System.Logger.Level.ERROR; import static java.lang.System.Logger.Level.ERROR;
import static java.text.MessageFormat.format; import static java.text.MessageFormat.format;
@@ -101,17 +103,28 @@ public class SqliteDb extends BaseDb implements WikiDb {
} }
} }
private int wikiPermissionCode(Permission perm){
return switch (perm){
case READ_ONLY -> 1;
case ASSIGNEE, EDIT, OWNER -> 2;
};
}
private Permission wikiPermission(int code){
return switch (code){
case 1 -> READ_ONLY;
case 2,3 -> EDIT;
default -> null;
};
}
@Override @Override
public Map<Long, Permission> loadMembers(WikiPage page) { public Map<Long, Permission> loadMembers(WikiPage page) {
try { try {
var map = new HashMap<Long, Permission>(); var map = new HashMap<Long, Permission>();
var rs = select(ALL).from(TABLE_PAGES_USERS).where(PAGE_ID,Condition.equal(page.id())).exec(db); var rs = select(ALL).from(TABLE_PAGES_USERS).where(PAGE_ID,Condition.equal(page.id())).exec(db);
while (rs.next()){ while (rs.next()){
var permission = switch (rs.getInt(PERMISSIONS)){ var permission = wikiPermission(rs.getInt(PERMISSIONS));
case 1 -> Permission.READ_ONLY;
case 2, 3 -> Permission.EDIT;
default -> null;
};
if (permission != null) map.put(rs.getLong(USER_ID),permission); if (permission != null) map.put(rs.getLong(USER_ID),permission);
} }
rs.close(); rs.close();
@@ -124,7 +137,13 @@ public class SqliteDb extends BaseDb implements WikiDb {
@Override @Override
public WikiPage save(WikiPage page) { public WikiPage save(WikiPage page) {
try { try {
insertInto(TABLE_PAGES,ID,VERSION,CONTENT).values(page.id(),page.version(),page.content()).execute(db).close(); if (page.isDirty(CONTENT) || page.isDirty(ID)) insertInto(TABLE_PAGES,ID,VERSION,CONTENT).values(page.id(),page.version(),page.content()).execute(db).close();
if (page.isDirty(MEMBERS)){
Query.delete().from(TABLE_PAGES_USERS).where(PAGE_ID,Condition.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);
for (var member : page.members().entrySet()) query.values(page.id(),member.getKey(),wikiPermissionCode(member.getValue().permission()));
query.execute(db).close();
}
return page; return page;
} catch (SQLException e) { } catch (SQLException e) {
throw databaseException("Failed to write wiki page \"{0}\" to database",page.id(),e); throw databaseException("Failed to write wiki page \"{0}\" to database",page.id(),e);

View File

@@ -116,6 +116,6 @@ public class WikiModule extends BaseHandler implements WikiService {
var member = page.members().get(user.id()); var member = page.members().get(user.id());
if (member == null || member.permission() != EDIT) throw forbidden("You are not allowed to edit {0}!",id); if (member == null || member.permission() != EDIT) throw forbidden("You are not allowed to edit {0}!",id);
var json = json(ex); var json = json(ex);
return sendContent(ex,wikiDb.save(page.patch(json))); return sendContent(ex,wikiDb.save(page.patch(json, userService())));
} }
} }