Merge remote-tracking branch 'origin/dev' into dev
This commit is contained in:
@@ -2,11 +2,15 @@
|
|||||||
package de.srsoftware.umbrella.core.model;
|
package de.srsoftware.umbrella.core.model;
|
||||||
|
|
||||||
import static de.srsoftware.umbrella.core.Constants.*;
|
import static de.srsoftware.umbrella.core.Constants.*;
|
||||||
|
import static de.srsoftware.umbrella.core.Util.markdown;
|
||||||
|
|
||||||
import de.srsoftware.umbrella.core.api.Owner;
|
import de.srsoftware.umbrella.core.api.Owner;
|
||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
public class DbLocation extends Location {
|
public class DbLocation extends Location {
|
||||||
private Owner owner;
|
private Owner owner;
|
||||||
@@ -14,6 +18,7 @@ public class DbLocation extends Location {
|
|||||||
private String name;
|
private String name;
|
||||||
private String description;
|
private String description;
|
||||||
private String relation; // when added to an item, this field describes the type of the relation
|
private String relation; // when added to an item, this field describes the type of the relation
|
||||||
|
private Set<String> dirtyFields = new HashSet<>();
|
||||||
|
|
||||||
public DbLocation(long id, Owner owner, Long parentLocationId, String name, String description){
|
public DbLocation(long id, Owner owner, Long parentLocationId, String name, String description){
|
||||||
super(id);
|
super(id);
|
||||||
@@ -23,10 +28,23 @@ public class DbLocation extends Location {
|
|||||||
this.description = description;
|
this.description = description;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DbLocation clear(){
|
||||||
|
dirtyFields.clear();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public String description() {
|
public String description() {
|
||||||
return description;
|
return description;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isDirty() {
|
||||||
|
return !dirtyFields.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDirty(String field){
|
||||||
|
return dirtyFields.contains(field);
|
||||||
|
}
|
||||||
|
|
||||||
public String name() {
|
public String name() {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
@@ -43,6 +61,27 @@ public class DbLocation extends Location {
|
|||||||
return parentLocationId;
|
return parentLocationId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DbLocation patch(JSONObject json) {
|
||||||
|
for (var field : json.keySet()){
|
||||||
|
boolean known = true;
|
||||||
|
switch (field) {
|
||||||
|
case PARENT_LOCATION_ID:
|
||||||
|
parentLocationId = json.getLong(field);
|
||||||
|
break;
|
||||||
|
case NAME:
|
||||||
|
name = json.getString(NAME);
|
||||||
|
break;
|
||||||
|
case DESCRIPTION:
|
||||||
|
description = json.getString(DESCRIPTION);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
known = false;
|
||||||
|
}
|
||||||
|
if (known) dirtyFields.add(field);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DbLocation resolve() {
|
public DbLocation resolve() {
|
||||||
return this;
|
return this;
|
||||||
@@ -53,7 +92,8 @@ public class DbLocation extends Location {
|
|||||||
var map = super.toMap();
|
var map = super.toMap();
|
||||||
map.put(OWNER,owner.toMap());
|
map.put(OWNER,owner.toMap());
|
||||||
map.put(NAME,name);
|
map.put(NAME,name);
|
||||||
map.put(DESCRIPTION,description);
|
map.put(DESCRIPTION,Map.of(SOURCE,description,RENDERED,markdown(description)));
|
||||||
|
if (parentLocationId != null) map.put(PARENT_LOCATION_ID,parentLocationId);
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,16 +4,19 @@
|
|||||||
import { error, yikes } from '../../warn.svelte';
|
import { error, yikes } from '../../warn.svelte';
|
||||||
import { t } from '../../translations.svelte';
|
import { t } from '../../translations.svelte';
|
||||||
|
|
||||||
import Locations from './Locations.svelte';
|
|
||||||
import ItemList from './ItemList.svelte';
|
import ItemList from './ItemList.svelte';
|
||||||
import ItemProps from './ItemProps.svelte';
|
import ItemProps from './ItemProps.svelte';
|
||||||
|
import LineEditor from '../../Components/LineEditor.svelte';
|
||||||
|
import Locations from './Locations.svelte';
|
||||||
|
import MarkdownEditor from '../../Components/MarkdownEditor.svelte';
|
||||||
import Notes from '../notes/RelatedNotes.svelte';
|
import Notes from '../notes/RelatedNotes.svelte';
|
||||||
import Tags from '../tags/TagList.svelte';
|
import Tags from '../tags/TagList.svelte';
|
||||||
|
|
||||||
let loc_data = $derived.by(loadLocation);
|
let loc_data = $derived.by(loadLocation);
|
||||||
let item = $state(null);
|
let item = $state(null);
|
||||||
let location = $state(null);
|
let location = $state(null);
|
||||||
let draggedItem = $state(null)
|
let draggedItem = $state(null)
|
||||||
|
let draggedLocation = $state(null)
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
// This effect runs whenever `location` changes
|
// This effect runs whenever `location` changes
|
||||||
@@ -37,6 +40,16 @@
|
|||||||
} else error(res);
|
} else error(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function drag_item(item){
|
||||||
|
draggedLocation = null;
|
||||||
|
draggedItem = item;
|
||||||
|
}
|
||||||
|
|
||||||
|
function drag_location(loc){
|
||||||
|
draggedItem = null;
|
||||||
|
draggedLocation = loc;
|
||||||
|
}
|
||||||
|
|
||||||
function dropNestedLocation(locations,loc){
|
function dropNestedLocation(locations,loc){
|
||||||
for (let [idx,entry] of locations.entries()){
|
for (let [idx,entry] of locations.entries()){
|
||||||
if (entry.id == loc.id){
|
if (entry.id == loc.id){
|
||||||
@@ -49,8 +62,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function move_dragged_to(new_loc){
|
async function move_dragged_to(new_loc){
|
||||||
const data = { item : draggedItem.id, target: new_loc.id };
|
const data = draggedItem ? { item : draggedItem.id, target: new_loc.id } : { parent_location_id: new_loc.id }
|
||||||
const url = api('stock/move_item');
|
const url = api(draggedItem ? 'stock/move_item' : `stock/location/${draggedLocation.id}`);
|
||||||
const res = await fetch(url,{
|
const res = await fetch(url,{
|
||||||
credentials : 'include',
|
credentials : 'include',
|
||||||
method : 'PATCH',
|
method : 'PATCH',
|
||||||
@@ -58,7 +71,14 @@
|
|||||||
});
|
});
|
||||||
if (res.ok){
|
if (res.ok){
|
||||||
yikes();
|
yikes();
|
||||||
location = new_loc;
|
location = new_loc;
|
||||||
|
if (!draggedItem){
|
||||||
|
for (var owner of top_level){
|
||||||
|
if (owner.locations && dropNestedLocation(owner.locations,draggedLocation)) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
draggedItem = null;
|
||||||
|
draggedLocation = null;
|
||||||
} else {
|
} else {
|
||||||
error(res);
|
error(res);
|
||||||
}
|
}
|
||||||
@@ -104,13 +124,28 @@
|
|||||||
loadProperties();
|
loadProperties();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function patchLocation(location,field,newValue){
|
||||||
|
const data = {};
|
||||||
|
data[field] = newValue;
|
||||||
|
console.log(data);
|
||||||
|
const url = api(`stock/location/${location.id}`);
|
||||||
|
const res = await fetch(url,{
|
||||||
|
credentials: 'include',
|
||||||
|
method:'PATCH',
|
||||||
|
body:JSON.stringify(data)
|
||||||
|
});
|
||||||
|
if (res.ok){
|
||||||
|
yikes();
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
error(res);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMount(load);
|
onMount(load);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<h2>{t('Stock')}</h2>
|
<h2>{t('Stock')}</h2>
|
||||||
<div class="grid3">
|
<div class="grid3">
|
||||||
<div class="locations">
|
<div class="locations">
|
||||||
@@ -118,7 +153,12 @@
|
|||||||
{#each top_level as realm,idx}
|
{#each top_level as realm,idx}
|
||||||
<h3>{realm.name}</h3>
|
<h3>{realm.name}</h3>
|
||||||
{#if realm.locations}
|
{#if realm.locations}
|
||||||
<Locations locations={realm.locations} parent={realm.parent} bind:selected={location} {move_dragged_to} />
|
<Locations
|
||||||
|
locations={realm.locations}
|
||||||
|
parent={realm.parent}
|
||||||
|
bind:selected={location}
|
||||||
|
{move_dragged_to}
|
||||||
|
drag_start={drag_location} />
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
@@ -128,9 +168,16 @@
|
|||||||
{:then data}
|
{:then data}
|
||||||
<div class="items">
|
<div class="items">
|
||||||
{#if location}
|
{#if location}
|
||||||
<h3>{location.name} <button class="symbol" title={t('delete_object',{object:t('location')})} onclick={e => deleteLocation(location)}></button></h3>
|
<h3>
|
||||||
|
<LineEditor editable={true} bind:value={location.name} type="span" onSet={newName => patchLocation(location,'name',newName)} />
|
||||||
|
<button class="symbol" title={t('delete_object',{object:t('location')})} onclick={e => deleteLocation(location)}></button>
|
||||||
|
{#if location.parent_location_id}
|
||||||
|
<button class="symbol" title={t('move_to_top')} onclick={e => patchLocation(location,'parent_location_id',0)}></button>
|
||||||
|
{/if}
|
||||||
|
</h3>
|
||||||
|
<MarkdownEditor editable={true} value={location.description} type="div" onSet={newDesc => patchLocation(location,'description',newDesc)} />
|
||||||
{/if}
|
{/if}
|
||||||
<ItemList items={data?.items.sort((a,b) => a.code.localeCompare(b.code))} bind:selected={item} drag_start={item => draggedItem = item} />
|
<ItemList items={data?.items.sort((a,b) => a.code.localeCompare(b.code))} bind:selected={item} drag_start={drag_item} />
|
||||||
</div>
|
</div>
|
||||||
<div class="properties">
|
<div class="properties">
|
||||||
<ItemProps {item} {properties} />
|
<ItemProps {item} {properties} />
|
||||||
@@ -146,4 +193,4 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/await}
|
{/await}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,13 @@
|
|||||||
|
|
||||||
import LineEditor from '../../Components/LineEditor.svelte';
|
import LineEditor from '../../Components/LineEditor.svelte';
|
||||||
|
|
||||||
let { locations, move_dragged_to = new_loc => {}, parent = null, selected = $bindable(null) } = $props();
|
let {
|
||||||
|
drag_start = loc => console.log({dragging:loc}),
|
||||||
|
locations,
|
||||||
|
move_dragged_to = new_loc => {},
|
||||||
|
parent = null,
|
||||||
|
selected = $bindable(null)
|
||||||
|
} = $props();
|
||||||
|
|
||||||
let show_location_form = $state(false);
|
let show_location_form = $state(false);
|
||||||
let new_location_name = $state(null);
|
let new_location_name = $state(null);
|
||||||
@@ -102,12 +108,14 @@
|
|||||||
{#each locations as location}
|
{#each locations as location}
|
||||||
<li onclick={e => toggleChildren(e, location)}
|
<li onclick={e => toggleChildren(e, location)}
|
||||||
class="{location.locations?'expanded':'collapsed'} {location.highlight?'highlight':null}"
|
class="{location.locations?'expanded':'collapsed'} {location.highlight?'highlight':null}"
|
||||||
|
draggable={true}
|
||||||
ondragover={e => drag_over(e,location)}
|
ondragover={e => drag_over(e,location)}
|
||||||
ondrop={e => onDrop(e,location)}
|
ondrop={e => onDrop(e,location)}
|
||||||
ondragleave={e => delete location.highlight}>
|
ondragleave={e => delete location.highlight}
|
||||||
|
ondragstart={e => drag_start(location)} >
|
||||||
<span class="name">{location.name}</span>
|
<span class="name">{location.name}</span>
|
||||||
{#if location.locations}
|
{#if location.locations}
|
||||||
<svelte:self locations={location.locations} {move_dragged_to} parent={{location:location.id}} bind:selected />
|
<svelte:self locations={location.locations} {drag_start} {move_dragged_to} parent={{location:location.id}} bind:selected />
|
||||||
{/if}
|
{/if}
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ public class Constants {
|
|||||||
public static final String ITEMS = "items";
|
public static final String ITEMS = "items";
|
||||||
public static final String LOCATIONS = "locations";
|
public static final String LOCATIONS = "locations";
|
||||||
public static final String MOVE_ITEM = "move_item";
|
public static final String MOVE_ITEM = "move_item";
|
||||||
|
public static final String MOVE_LOCATION = "move_location";
|
||||||
public static final String OF_USER = "of_user";
|
public static final String OF_USER = "of_user";
|
||||||
public static final String PARENT = "parent";
|
public static final String PARENT = "parent";
|
||||||
public static final String PROPERTY_ID = "prop_id";
|
public static final String PROPERTY_ID = "prop_id";
|
||||||
|
|||||||
@@ -288,11 +288,12 @@ public class SqliteDb extends BaseDb implements StockDb {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Location save(DbLocation location) {
|
public DbLocation save(DbLocation location) {
|
||||||
|
var parentId = location.parent() == 0 ? null : location.parent();
|
||||||
if (location.id() == 0) { // new location
|
if (location.id() == 0) { // new location
|
||||||
try {
|
try {
|
||||||
var rs = insertInto(TABLE_LOCATIONS,OWNER,PARENT_LOCATION_ID,NAME,DESCRIPTION)
|
var rs = insertInto(TABLE_LOCATIONS,OWNER,PARENT_LOCATION_ID,NAME,DESCRIPTION)
|
||||||
.values(location.owner().dbCode(),location.parent(),location.name(),null)
|
.values(location.owner().dbCode(),location.parent() == 0 ? null : parentId,location.name(),location.description())
|
||||||
.execute(db).getGeneratedKeys();
|
.execute(db).getGeneratedKeys();
|
||||||
long id = 0;
|
long id = 0;
|
||||||
if (rs.next()) id = rs.getLong(1);
|
if (rs.next()) id = rs.getLong(1);
|
||||||
@@ -303,7 +304,17 @@ public class SqliteDb extends BaseDb implements StockDb {
|
|||||||
throw databaseException("Failed to save new location ({0})",location.name());
|
throw databaseException("Failed to save new location ({0})",location.name());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw databaseException("Updating locations not implemented");
|
try {
|
||||||
|
update(TABLE_LOCATIONS)
|
||||||
|
.set(OWNER, PARENT_LOCATION_ID, NAME, DESCRIPTION)
|
||||||
|
.where(ID,equal(location.id()))
|
||||||
|
.prepare(db)
|
||||||
|
.apply(location.owner().dbCode(), parentId, location.name(), location.description())
|
||||||
|
.close();
|
||||||
|
return location.clear();
|
||||||
|
} catch (SQLException e){
|
||||||
|
throw databaseException("Updating location \"{0}\" not implemented",location.name());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ public interface StockDb {
|
|||||||
Collection<DbLocation> listUserLocations(UmbrellaUser userId);
|
Collection<DbLocation> listUserLocations(UmbrellaUser userId);
|
||||||
Item loadItem(long id);
|
Item loadItem(long id);
|
||||||
DbLocation loadLocation(long locationId);
|
DbLocation loadLocation(long locationId);
|
||||||
Location save(DbLocation location);
|
DbLocation save(DbLocation location);
|
||||||
Item save(Item item);
|
Item save(Item item);
|
||||||
Property setProperty(long itemId, long existingPropId, Object value);
|
Property setProperty(long itemId, long existingPropId, Object value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,7 +113,16 @@ public class StockModule extends BaseHandler implements StockService {
|
|||||||
var user = userService().loadUser(token);
|
var user = userService().loadUser(token);
|
||||||
if (user.isEmpty()) return unauthorized(ex);
|
if (user.isEmpty()) return unauthorized(ex);
|
||||||
return switch (path.pop()){
|
return switch (path.pop()){
|
||||||
case MOVE_ITEM -> patchMove(user.get(), path,ex);
|
case MOVE_ITEM -> patchMoveItem(user.get(), path,ex);
|
||||||
|
case LOCATION -> {
|
||||||
|
try {
|
||||||
|
var id = Long.parseLong(path.pop());
|
||||||
|
yield patchLocation(id, user.get(), ex);
|
||||||
|
} catch (NumberFormatException nfe){
|
||||||
|
yield super.doPatch(path,ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
case null -> patchItem(user.get(),ex);
|
case null -> patchItem(user.get(),ex);
|
||||||
default -> super.doPatch(path,ex);
|
default -> super.doPatch(path,ex);
|
||||||
};
|
};
|
||||||
@@ -213,7 +222,7 @@ public class StockModule extends BaseHandler implements StockService {
|
|||||||
return sendContent(ex,stockDb.save(item));
|
return sendContent(ex,stockDb.save(item));
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean patchMove(UmbrellaUser user, Path path, HttpExchange ex) throws IOException {
|
private boolean patchMoveItem(UmbrellaUser user, Path path, HttpExchange ex) throws IOException {
|
||||||
var json = json(ex);
|
var json = json(ex);
|
||||||
if (!(json.get(ITEM) instanceof Number itemId)) throw missingFieldException(ITEM);
|
if (!(json.get(ITEM) instanceof Number itemId)) throw missingFieldException(ITEM);
|
||||||
if (!(json.get(TARGET) instanceof Number locationId)) throw missingFieldException(TARGET);
|
if (!(json.get(TARGET) instanceof Number locationId)) throw missingFieldException(TARGET);
|
||||||
@@ -232,6 +241,25 @@ public class StockModule extends BaseHandler implements StockService {
|
|||||||
return sendContent(ex,item);
|
return sendContent(ex,item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean patchLocation(long locationId, UmbrellaUser user, HttpExchange ex) throws IOException {
|
||||||
|
var json = json(ex);
|
||||||
|
var location = stockDb.loadLocation(locationId);
|
||||||
|
var owner = location.owner().resolve();
|
||||||
|
if (!assigned(owner,user)) throw forbidden("You are not allowed to edit \"{0}\"!",location.name());
|
||||||
|
|
||||||
|
if (json.has(PARENT_LOCATION_ID) && json.get(PARENT_LOCATION_ID) instanceof Number parentId){
|
||||||
|
if (parentId.longValue() != 0L) {
|
||||||
|
var target = stockDb.loadLocation(parentId.longValue());
|
||||||
|
var targetOwner = target.owner().resolve();
|
||||||
|
if (!assigned(targetOwner, user)) throw forbidden("You are not allowed to edit \"{0}\"!", target.name());
|
||||||
|
if (!targetOwner.equals(owner)) throw unprocessable("You may not move locations from one owner ({0}) to another ({1})", owner, targetOwner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
location = stockDb.save(location.patch(json));
|
||||||
|
return sendContent(ex,location);
|
||||||
|
}
|
||||||
|
|
||||||
private boolean postLocation(UmbrellaUser user, HttpExchange ex) throws IOException {
|
private boolean postLocation(UmbrellaUser user, HttpExchange ex) throws IOException {
|
||||||
var json = json(ex);
|
var json = json(ex);
|
||||||
if (!(json.get(NAME) instanceof String name)) throw missingFieldException(NAME);
|
if (!(json.get(NAME) instanceof String name)) throw missingFieldException(NAME);
|
||||||
|
|||||||
@@ -170,6 +170,7 @@
|
|||||||
"wiki": "Wiki"
|
"wiki": "Wiki"
|
||||||
},
|
},
|
||||||
"month": "Monat",
|
"month": "Monat",
|
||||||
|
"move_to_top": "nach ganz oben bewegen",
|
||||||
"must_not_be_empty": "darf nicht leer sein",
|
"must_not_be_empty": "darf nicht leer sein",
|
||||||
|
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
|
|||||||
@@ -169,6 +169,7 @@
|
|||||||
"wiki": "wiki"
|
"wiki": "wiki"
|
||||||
},
|
},
|
||||||
"month": "month",
|
"month": "month",
|
||||||
|
"move_to_top": "move to top level",
|
||||||
"must_not_be_empty": "must not be empty",
|
"must_not_be_empty": "must not be empty",
|
||||||
|
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
|
|||||||
Reference in New Issue
Block a user