Browse Source

completed user-defined states

feature/entityId
Stephan Richter 3 months ago
parent
commit
64e925c83f
  1. 19
      core/src/main/java/de/srsoftware/umbrella/core/model/Status.java
  2. 2
      frontend/src/Components/StateSelector.svelte
  3. 23
      frontend/src/routes/project/Kanban.svelte
  4. 30
      frontend/src/routes/project/View.svelte
  5. 2
      project/src/main/java/de/srsoftware/umbrella/project/Constants.java
  6. 4
      project/src/main/java/de/srsoftware/umbrella/project/ProjectDb.java
  7. 37
      project/src/main/java/de/srsoftware/umbrella/project/ProjectModule.java
  8. 99
      project/src/main/java/de/srsoftware/umbrella/project/SqliteDb.java

19
core/src/main/java/de/srsoftware/umbrella/core/model/Status.java

@ -1,10 +1,18 @@ @@ -1,10 +1,18 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.core.model;
import de.srsoftware.tools.Mappable;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static de.srsoftware.umbrella.core.Constants.CODE;
import static de.srsoftware.umbrella.core.Constants.NAME;
public record Status(String name, int code){
public record Status(String name, int code) implements Mappable {
public static final Status OPEN = new Status("OPEN",10);
public static final Status STARTED = new Status("STARTED",20);
public static final Status PENDING = new Status("PENDING", 40);
@ -22,4 +30,13 @@ public record Status(String name, int code){ @@ -22,4 +30,13 @@ public record Status(String name, int code){
default -> throw new IllegalArgumentException();
};
}
public static Status of(ResultSet rs) throws SQLException {
return new Status(rs.getString(NAME),rs.getInt(CODE));
}
@Override
public Map<String, Object> toMap() {
return Map.of(NAME,name,CODE,code);
}
}

2
frontend/src/Components/StateSelector.svelte

@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
{#if project?.allowed_states}
<select bind:value={selected} onchange={() => onchange(selected)}>
{#each Object.entries(project.allowed_states) as [code,name]}
<option value={+code}>{t('state_'+name.toLowerCase())}</option>
<option value={+code}>{code%10?name:t('state_'+name.toLowerCase())}</option>
{/each}
</select>
{/if}

23
frontend/src/routes/project/Kanban.svelte

@ -19,10 +19,9 @@ @@ -19,10 +19,9 @@
let project = $state(null);
let ready = $state(false);
let router = useTinyRouter();
let states = $state(null);
let tasks = $state({});
let users = {};
let columns = $derived(states?Object.keys(states).length+1:1);
let columns = $derived(project.allowed_states?Object.keys(project.allowed_states).length+1:1);
async function create(name,user_id,state){
var url = api('task/add');
@ -85,7 +84,6 @@ @@ -85,7 +84,6 @@
async function load(){
await loadProject();
await loadStates();
await loadTasks({project_id:+id,parent_task_id:0});
ready = true;
loadTags();
@ -107,17 +105,6 @@ @@ -107,17 +105,6 @@
}
}
async function loadStates(){
const url = api(`project/${id}/states`);
const resp = await fetch(url,{credentials:'include'});
if (resp.ok){
states = await resp.json();
error = null;
} else {
error = await resp.text();
}
}
async function loadTag(task){
const url = api(`tags/task/${task.id}`);
const resp = await fetch(url,{credentials:'include'});
@ -194,14 +181,14 @@ @@ -194,14 +181,14 @@
{t('filter')}
</span>
<div class="head">{t('user')}</div>
{#if states}
{#each Object.entries(states) as [sid,state]}
<div class="head">{t('state_'+state.toLowerCase())}</div>
{#if project.allowed_states}
{#each Object.entries(project.allowed_states) as [sid,state]}
<div class="head">{sid%10?state:t('state_'+state.toLowerCase())}</div>
{/each}
{/if}
{#each Object.entries(tasks) as [uid,stateList]}
<div class="user">{users[uid]}</div>
{#each Object.entries(states) as [state,name]}
{#each Object.entries(project.allowed_states) as [state,name]}
<div class={['state_'+state, highlight.user == uid && highlight.state == state ? 'highlight':'']} ondragover={ev => hover(ev,uid,state)} ondrop={ev => drop(uid,state)} >
{#if stateList[state]}
{#each Object.values(stateList[state]).sort((a,b) => a.name.localeCompare(b.name)) as task}

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

@ -21,11 +21,30 @@ @@ -21,11 +21,30 @@
let showSettings = $state(false);
let tasks = $state(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]);
async function addMember(entry){
const ids = Object.keys(entry);
if (ids) update({new_member:+ids.pop()});
}
async function addState(){
const url = api(`project/${id}/state`);
const resp = await fetch(url,{
credentials: 'include',
method: 'POST',
body: JSON.stringify(new_state)
});
if (resp.ok){
const json = await resp.json();
project.allowed_states[json.code] = json.name;
error = null;
} else {
error = await resp.text();
}
}
function addTask(){
router.navigate(`/project/${id}/add_task`);
}
@ -200,6 +219,17 @@ @@ -200,6 +219,17 @@
</td>
</tr>
{/each}
<tr>
<th>
<input type="number" bind:value={new_state.code} />
</th>
<td>
<input type="text" bind:value={new_state.name} />
{#if state_available}
<button onclick={addState} >{t('add_state')}</button>
{/if}
</td>
</tr>
{/if}
{/if}
{#if estimated_time.sum}

2
project/src/main/java/de/srsoftware/umbrella/project/Constants.java

@ -3,9 +3,11 @@ package de.srsoftware.umbrella.project; @@ -3,9 +3,11 @@ package de.srsoftware.umbrella.project;
public class Constants {
private Constants(){}
public static final String CONFIG_DATABASE = "umbrella.modules.project.database";
public static final String DB_VERSION = "project_db_version";
public static final String PERMISSIONS = "permissions";
public static final String TABLE_CUSTOM_STATES = "custom_states";
public static final String TABLE_PROJECTS = "projects";
public static final String TABLE_PROJECT_USERS = "projects_users";

4
project/src/main/java/de/srsoftware/umbrella/project/ProjectDb.java

@ -4,6 +4,8 @@ package de.srsoftware.umbrella.project; @@ -4,6 +4,8 @@ package de.srsoftware.umbrella.project;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.core.model.Permission;
import de.srsoftware.umbrella.core.model.Project;
import de.srsoftware.umbrella.core.model.Status;
import java.util.Map;
public interface ProjectDb {
@ -14,4 +16,6 @@ public interface ProjectDb { @@ -14,4 +16,6 @@ public interface ProjectDb {
Map<Long, Project> ofUser(long userId, boolean includeClosed) throws UmbrellaException;
Project save(Project prj) throws UmbrellaException;
Status save(long projectId, Status newState);
}

37
project/src/main/java/de/srsoftware/umbrella/project/ProjectModule.java

@ -12,6 +12,7 @@ import static de.srsoftware.umbrella.core.model.Status.OPEN; @@ -12,6 +12,7 @@ import static de.srsoftware.umbrella.core.model.Status.OPEN;
import static de.srsoftware.umbrella.core.model.Status.PREDEFINED;
import static de.srsoftware.umbrella.project.Constants.CONFIG_DATABASE;
import static java.lang.Boolean.TRUE;
import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
import static java.net.HttpURLConnection.HTTP_OK;
import com.sun.net.httpserver.HttpExchange;
@ -73,7 +74,6 @@ public class ProjectModule extends BaseHandler implements ProjectService { @@ -73,7 +74,6 @@ public class ProjectModule extends BaseHandler implements ProjectService {
head = path.pop();
yield switch (head) {
case null -> getProject(ex, projectId, user.get());
case STATES -> super.doGet(path, ex); // TODO THIS SHOULD NO LONGER BE REQUIRED
default -> super.doGet(path, ex);
};
}
@ -121,15 +121,30 @@ public class ProjectModule extends BaseHandler implements ProjectService { @@ -121,15 +121,30 @@ public class ProjectModule extends BaseHandler implements ProjectService {
if (user.isEmpty()) return unauthorized(ex);
var head = path.pop();
return switch (head) {
case LIST -> postProjectList(ex,user.get());
case null -> postProject(ex,user.get());
default -> super.doGet(path,ex);
case LIST -> postProjectList(ex, user.get());
case null -> postProject(ex, user.get());
default -> {
var projectId = Long.parseLong(head);
head = path.pop();
yield switch (head){
case STATE -> postNewState(ex,projectId,user.get());
case null, default -> super.doGet(path, ex);
};
}
};
} catch (NumberFormatException e){
return sendContent(ex,HTTP_BAD_REQUEST,"Invalid project id");
} catch (UmbrellaException e){
return send(ex,e);
}
}
private void dropMember(Project project, long userId) {
if (project.members().get(userId).permission() == OWNER) throw forbidden("You may not remove the owner of the project");
projects.dropMember(project.id(),userId);
project.members().remove(userId);
}
private boolean getProject(HttpExchange ex, long projectId, UmbrellaUser user) throws IOException, UmbrellaException {
var project = loadMembers(projects.load(projectId));
if (!project.hasMember(user)) throw forbidden("You are not a member of {0}",project.name());
@ -216,12 +231,16 @@ public class ProjectModule extends BaseHandler implements ProjectService { @@ -216,12 +231,16 @@ public class ProjectModule extends BaseHandler implements ProjectService {
return sendContent(ex,project.toMap());
}
private void dropMember(Project project, long userId) {
if (project.members().get(userId).permission() == OWNER) throw forbidden("You may not remove the owner of the project");
projects.dropMember(project.id(),userId);
project.members().remove(userId);
}
private boolean postNewState(HttpExchange ex, long projectId, UmbrellaUser user) throws IOException {
var project = loadMembers(load(projectId));
if (!project.hasMember(user)) throw forbidden("You are not a member of {0}",project.name());
var json = json(ex);
if (!(json.has(CODE) && json.get(CODE) instanceof Number code)) throw missingFieldException(CODE);
if (!(json.has(NAME) && json.get(NAME) instanceof String name)) throw missingFieldException(NAME);
var newState = new Status(name,code.intValue());
return sendContent(ex,projects.save(projectId,newState));
}
private boolean postProject(HttpExchange ex, UmbrellaUser user) throws IOException, UmbrellaException {
var json = json(ex);

99
project/src/main/java/de/srsoftware/umbrella/project/SqliteDb.java

@ -17,7 +17,10 @@ import de.srsoftware.tools.jdbc.Query; @@ -17,7 +17,10 @@ import de.srsoftware.tools.jdbc.Query;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.core.model.Permission;
import de.srsoftware.umbrella.core.model.Project;
import de.srsoftware.umbrella.core.model.Status;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
@ -34,40 +37,60 @@ public class SqliteDb implements ProjectDb { @@ -34,40 +37,60 @@ public class SqliteDb implements ProjectDb {
init();
}
private int createTables() {
createProjectTables();
return createSettingsTable();
}
private void createProjectTables() {
private void createProjectTable() {
var createTable = """
CREATE TABLE IF NOT EXISTS {0} (
`{1}` INTEGER PRIMARY KEY,
`{2}` INTEGER,
`{3}` VARCHAR(255) NOT NULL,
`{4}` TEXT,
`{5}` INT DEFAULT {6},
`{7}` BOOLEAN DEFAULT 0
)""";
CREATE TABLE IF NOT EXISTS {0} (
`{1}` INTEGER PRIMARY KEY,
`{2}` INTEGER,
`{3}` VARCHAR(255) NOT NULL,
`{4}` TEXT,
`{5}` INT DEFAULT {6},
`{7}` BOOLEAN DEFAULT 0
)""";
try {
var stmt = db.prepareStatement(format(createTable,TABLE_PROJECTS, ID, COMPANY_ID, NAME, DESCRIPTION, STATUS, OPEN.code(), SHOW_CLOSED));
var stmt = db.prepareStatement(format(createTable, TABLE_PROJECTS, ID, COMPANY_ID, NAME, DESCRIPTION, STATUS, OPEN.code(), SHOW_CLOSED));
stmt.execute();
stmt.close();
} catch (SQLException e) {
LOG.log(ERROR,ERROR_FAILED_CREATE_TABLE,TABLE_PROJECTS ,e);
LOG.log(ERROR, ERROR_FAILED_CREATE_TABLE, TABLE_PROJECTS, e);
throw new RuntimeException(e);
}
}
long count = 0L;
private void createStatesTable(){
var sql = """
CREATE TABLE IF NOT EXISTS custom_states (
project_id LONG NOT NULL,
code INT NOT NULL,
name VARCHAR(64) NOT NULL,
PRIMARY KEY (project_id, code)
);
""";
try {
ResultSet rs = select("COUNT(*)").from(TABLE_PROJECTS).exec(db);
if (rs.next()) count = rs.getLong(1);
rs.close();
} catch (SQLException ignored) {
// go on with table creation
var stmt = db.prepareStatement(format(sql));
stmt.execute();
stmt.close();
} catch (SQLException e) {
LOG.log(ERROR, ERROR_FAILED_CREATE_TABLE, TABLE_CUSTOM_STATES, e);
throw new RuntimeException(e);
}
createTable = """
}
private int createTables() {
int currentVersion = createSettingsTable();
switch (currentVersion){
case 0:
createProjectTable();
createUsersTable();
case 1:
createStatesTable();
}
return setCurrentVersion(2);
}
private void createUsersTable(){
var createTable = """
CREATE TABLE IF NOT EXISTS {0} (
{1} INT NOT NULL,
{2} INT NOT NULL,
@ -97,15 +120,11 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255) @@ -97,15 +120,11 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255)
throw new RuntimeException(e);
}
Integer version = null;
var version = 0;
try {
var rs = Query.select(VALUE).from(TABLE_SETTINGS).where(KEY, equal(DB_VERSION)).exec(db);
if (rs.next()) version = rs.getInt(VALUE);
rs.close();
if (version == null) {
version = INITIAL_DB_VERSION;
insertInto(TABLE_SETTINGS, KEY, VALUE).values(DB_VERSION,version).execute(db).close();
}
return version;
} catch (SQLException e) {
@ -152,6 +171,10 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255) @@ -152,6 +171,10 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255)
if (rs.next()) result = Project.of(rs);
rs.close();
if (result == null) throw UmbrellaException.notFound("No project found for id {0}",projectId);
rs = select(ALL).from(TABLE_CUSTOM_STATES).where(PROJECT_ID,equal(projectId)).exec(db);
var states = result.allowedStates();
while (rs.next()) states.add(Status.of(rs));
rs.close();
return result;
} catch (SQLException e) {
throw new UmbrellaException("Failed to load project from database");
@ -236,4 +259,24 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255) @@ -236,4 +259,24 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255)
}
return null;
}
@Override
public Status save(long projectId, Status newState) {
try {
insertInto(TABLE_CUSTOM_STATES,PROJECT_ID,CODE,NAME).values(projectId,newState.code(),newState.name()).execute(db).close();
return newState;
} catch (SQLException e) {
throw new UmbrellaException("Failed to create custom state!");
}
}
private int setCurrentVersion(int version) {
try {
replaceInto(TABLE_SETTINGS, KEY, VALUE).values(DB_VERSION,version).execute(db).close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
return version;
}
}

Loading…
Cancel
Save