7 Commits

9 changed files with 424 additions and 184 deletions

View File

@@ -0,0 +1,101 @@
<script lang="ts">
import Edit from '$lib/icons/Edit.svelte';
import Trash from '$lib/icons/Trash.svelte';
import { tick } from 'svelte';
interface ListItem {
name: string;
token?: string;
// add other properties as needed
}
let {
list,
editedName = $bindable(),
currentName,
onSave = () => {},
onDelete = () => {}
} = $props();
let names = list.map((l: ListItem) => l.name);
let localName = $state(currentName);
let wasCancelled = $state(false);
// Automatisch berechneter Fehler
let error: string = $derived(validateName(localName));
// Manuell steuerbarer Fehlerstatus
let manualError = $state('');
console.log('EditableItem: Beginn', names, editedName);
let isEditing = $state(false);
let inputRef: HTMLInputElement;
// Validierungsfunktion
function validateName(name: string) {
const trimmed = name.trim();
if (!trimmed) {
return 'Name darf nicht leer sein.';
}
const duplicate = list.some(
(item: ListItem) => item.name === trimmed && item.name !== currentName
);
if (duplicate) return 'Name existiert bereits.';
return '';
}
// Speichern, wenn gültig und zurück an Eltern
function commitIfValid() {
if (!error && !wasCancelled && localName != currentName) {
editedName = localName.trim();
onSave(editedName, currentName); //Eltern benachrichtigen
} else {
localName = currentName;
resetEdit();
}
}
// Abbrechen Eingabe zurücksetzen
function resetEdit() {
wasCancelled = false;
manualError = '';
inputRef?.blur();
isEditing = false;
}
// Tastatursteuerung
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Enter') {
event.preventDefault();
commitIfValid();
} else if (event.key === 'Escape') {
event.preventDefault();
localName = currentName;
resetEdit();
}
}
async function startEdit() {
isEditing = true;
await tick();
inputRef?.focus();
}
</script>
<div>
<input
bind:this={inputRef}
bind:value={localName}
onblur={commitIfValid}
onkeydown={handleKeydown}
/>
<button onclick={startEdit}><Edit /></button>
<button onclick={() => onDelete(currentName)}><Trash /></button>
{#if manualError || error}
<p style="color: red;">{manualError || error}</p>
{/if}
</div>

View File

@@ -0,0 +1,27 @@
/**
*
* @param oldValue
* @param inputValue
* @param options
* @returns
*/
export function validateNameInput(oldValue:string, inputValue: string, options: { minLength?: number; existingNames?: string[] }) {
const errors: string[] = [];
if (!inputValue.trim()) errors.push('Feld darf nicht leer sein');
if (options.existingNames?.includes(inputValue) && oldValue !== inputValue)
errors.push('Name existiert bereits');
return errors;
}
// siehe suche: fail
// Button
// BaseInputField
// fetch siehe b038
// api/list/vorgang/tatort/server.ts
// api/list/vorgang/server.ts

View File

@@ -1,10 +1,15 @@
<script>
let classNames = '';
export { classNames as class };
</script>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
class=" w-6 h-6" class=" w-6 h-6 {classNames}"
> >
<path <path
stroke-linecap="round" stroke-linecap="round"

Before

Width:  |  Height:  |  Size: 483 B

After

Width:  |  Height:  |  Size: 571 B

View File

@@ -1,11 +1,21 @@
<script lang="ts"> <script lang="ts">
import Trash from '$lib/icons/Trash.svelte';
import Folder from '$lib/icons/Folder.svelte'; import Folder from '$lib/icons/Folder.svelte';
import type { PageData } from '../$types'; import EditableItem from '$lib/components/EditableItem.svelte';
import { invalidate } from '$app/navigation';
export let data: PageData; interface ListItem {
name: string;
token?: string;
// add other properties as needed
}
const caseList = data.caseList; let { data } = $props();
const caseList: ListItem[] = data.caseList;
//Variabeln für EditableItem
let names: string[] = $state(caseList.map((l) => l.name));
let editedName: string = $state('');
async function delete_item(ev: Event) { async function delete_item(ev: Event) {
let delete_item = window.confirm('Bist du sicher?'); let delete_item = window.confirm('Bist du sicher?');
@@ -14,7 +24,7 @@
const target = ev.currentTarget as HTMLElement | null; const target = ev.currentTarget as HTMLElement | null;
if (!target) return; if (!target) return;
let filename = target.id.split('del__')[1]; let filename = target.id.split('del__')[1];
// delete request // delete request
// -------------- // --------------
@@ -36,6 +46,56 @@
} }
} }
} }
async function handleSave(newName: string, oldName: string) {
console.log('Eltern, speichern erfolgreich', newName, oldName);
try {
const res = await fetch(`/api/list/${oldName}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ oldName, newName })
});
if (!res.ok) {
const msg = await res.text();
console.error('❌ Fehler beim Speichern:', msg);
} else {
console.log('✅ Erfolgreich gespeichert:', newName);
}
} catch (err) {
console.error('⚠️ Netzwerkfehler:', err);
}
}
async function handleDelete(vorgang: string) {
let url = `/api/list/${vorgang}`;
try {
const res = await fetch(url, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ vorgang })
});
if (!res.ok) {
const msg = await res.text();
console.error('❌ Fehler beim Löschen:', msg);
} else {
setTimeout(() => {
window.location.reload();
}, 500);
//await invalidate('/api/list');
console.log('🗑️ Erfolgreich gelöscht:', vorgang);
await invalidate('/api/list'); // funktioniert nicht vernünftig
}
} catch (err) {
console.error('⚠️ Netzwerkfehler beim Löschen:', err);
}
}
</script> </script>
<div class="-z-10 bg-white"> <div class="-z-10 bg-white">
@@ -44,29 +104,30 @@
</div> </div>
<div class="mx-auto flex justify-center max-w-7xl h-full"> <div class="mx-auto flex justify-center max-w-7xl h-full">
<ul role="list" class="divide-y divide-gray-100"> <ul role="list" class="divide-y divide-gray-100">
{#each caseList as item} {#each caseList as item, i}
<li> <li>
<a href="/list/{item.name}?token={item.token}" class="flex justify-between gap-x-6 py-5"> <div class="flex gap-x-4">
<div class="flex gap-x-4"> <!-- Ordner -->
<!-- Ordner --> <a
href="/list/{item.name}?token={item.token}"
class="bg-red-500 flex justify-between gap-x-6 py-5"
>
<Folder /> <Folder />
<div class="min-w-0 flex-auto"> </a>
<span class="text-sm font-semibold leading-6 text-gray-900">{item.name}</span>
<!-- Delete button --> <div class="min-w-0 flex-auto">
<button <EditableItem
style="padding: 2px" list={caseList}
id="del__{item.name}" bind:editedName={names[i]}
on:click|preventDefault={delete_item} currentName={item.name}
aria-label="Vorgang {item.name} löschen" onSave={handleSave}
> onDelete={handleDelete}
<Trash /> ></EditableItem>
</button>
</div>
</div> </div>
<div class="hidden sm:flex sm:flex-col sm:items-end"> <div class="hidden sm:flex sm:flex-col sm:items-end">
<p class="text-sm leading-6 text-gray-900">Vorgang</p> <p class="text-sm leading-6 text-gray-900">Vorgang</p>
</div> </div>
</a> </div>
</li> </li>
{/each} {/each}
</ul> </ul>

View File

@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
//Kann komplett gelöscht werden
import Alert from '$lib/components/Alert.svelte'; import Alert from '$lib/components/Alert.svelte';
import Button from '$lib/components/Button.svelte'; import Button from '$lib/components/Button.svelte';
import Modal from '$lib/components/Modal/Modal.svelte'; import Modal from '$lib/components/Modal/Modal.svelte';
@@ -75,19 +76,14 @@
</div> </div>
<label for="code"> <label for="code">
<span >Zugangscode (optional) </span> <span>Zugangscode (optional) </span>
</label> </label>
<div class="mt-2"> <div class="mt-2">
<div <div>
> <input type="text" id="code" />
<input </div>
type="text"
id="code"
/>
</div> </div>
</div>
</div> </div>
</div> </div>

View File

@@ -8,6 +8,7 @@ export const load: PageServerLoad = async ({ params, url }) => {
const crimesList = await getVorgangByCaseId(caseId); const crimesList = await getVorgangByCaseId(caseId);
return { return {
caseId,
crimesList, crimesList,
caseToken caseToken
}; };

View File

@@ -1,64 +1,55 @@
<script lang="ts"> <script lang="ts">
import shortenFileSize from '$lib/helper/shortenFileSize';
import { page } from '$app/stores'; import { page } from '$app/stores';
import timeElapsed from '$lib/helper/timeElapsed';
import Alert from '$lib/components/Alert.svelte'; import Alert from '$lib/components/Alert.svelte';
import Button from '$lib/components/Button.svelte'; import Button from '$lib/components/Button.svelte';
import Modal from '$lib/components/Modal/Modal.svelte'; import Modal from '$lib/components/Modal/Modal.svelte';
import ModalTitle from '$lib/components/Modal/ModalTitle.svelte'; import ModalTitle from '$lib/components/Modal/ModalTitle.svelte';
import ModalContent from '$lib/components/Modal/ModalContent.svelte'; import ModalContent from '$lib/components/Modal/ModalContent.svelte';
import ModalFooter from '$lib/components/Modal/ModalFooter.svelte'; import ModalFooter from '$lib/components/Modal/ModalFooter.svelte';
import EditableItem from '$lib/components/EditableItem.svelte';
import Cube from '$lib/icons/Cube.svelte'; import Cube from '$lib/icons/Cube.svelte';
import Edit from '$lib/icons/Edit.svelte'; import shortenFileSize from '$lib/helper/shortenFileSize.js';
import Trash from '$lib/icons/Trash.svelte'; import timeElapsed from '$lib/helper/timeElapsed.js';
/** export let data; */ let { data } = $props();
/** @type {import('./$types').PageData} */
export let data;
interface ListItem { interface ListItem {
name: string; name: string;
size: number; size?: number;
lastModified: string | number | Date; lastModified?: string | number | Date | undefined;
show_button?: boolean; show_button?: boolean;
// add other properties as needed // add other properties as needed
} }
const crimesList: ListItem[] = data.crimesList; const crimesList: ListItem[] = data.crimesList;
const token: string = data.caseToken; const token: string = data.caseToken;
let vorgang = data.caseId;
/* export let vorgang = $page.params.vorgang; */
let open = false; //Variabeln für EditableItem
$: open; let names: string[] = $state(crimesList.map((l) => l.name));
let inProgress = false; let editedName: string = $state('');
$: inProgress;
let err = false; let open = $state(false);
$: err; let inProgress = $state(false);
let err = $state(false);
let editingId: number;
// let admin = data?.user?.admin;
let admin = true;
let rename_input; let rename_input;
$: rename_input;
$effect(() => {
console.log('rename_input hat sich geändert:', rename_input);
});
function uploadSuccessful() { function uploadSuccessful() {
open = false; open = false;
} }
function defocus_element(i: number) { /* async function handle_input(ev: KeyboardEvent, i: number) {
let item = crimesList[i];
let text_field_id = `label__${item.name}`;
let text_field = document.getElementById(text_field_id);
if (text_field) {
text_field.setAttribute('contenteditable', 'false');
text_field.textContent = item.name;
}
// reshow button
crimesList[i].show_button = true;
return;
}
async function handle_input(ev: KeyboardEvent, i: number) {
let item = crimesList[i]; let item = crimesList[i];
if (ev.key == 'Escape') { if (ev.key == 'Escape') {
let text_field_id = `label__${item.name}`; let text_field_id = `label__${item.name}`;
@@ -127,125 +118,114 @@
return; return;
} }
} */
async function handleSave(newName: string, oldName: string) {
console.log('Eltern, speichern erfolgreich', newName, oldName);
try {
const res = await fetch(`/api/list/${vorgang}/${oldName}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ oldName, newName })
});
if (!res.ok) {
const msg = await res.text();
console.error('❌ Fehler beim Speichern:', msg);
} else {
console.log('✅ Erfolgreich gespeichert:', newName);
}
} catch (err) {
console.error('⚠️ Netzwerkfehler:', err);
}
}
async function handleDelete(tatort: string) {
// delete request
// --------------
/* let url = new URL($page.url);
url.pathname += `/${filename}`;
try {
const response = await fetch(`/api${url.pathname}`, { method: 'DELETE' }); */
let url = new URL($page.url);
url.pathname += `/${tatort}`;
console.log('Delete tatort: ', `/api${url.pathname}`, url.pathname);
try {
const res = await fetch(`/api${url.pathname}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ vorgang, tatort })
});
console.log('res delete', res);
if (!res.ok) {
const msg = await res.text();
console.error('❌ Fehler beim Löschen:', msg);
} else {
console.log('🗑️ Erfolgreich gelöscht:', url.pathname);
}
} catch (err) {
console.error('⚠️ Netzwerkfehler beim Löschen:', err);
}
} }
</script> </script>
<div class="-z-10 bg-white"> <div class="-z-10 bg-white">
<div class="flex flex-col items-center justify-center w-full"> <div class="flex flex-col items-center justify-center w-full">
<h1 class="text-xl">Vorgang {$page.params.vorgang}</h1> <h1 class="text-xl">Vorgang {vorgang}</h1>
</div> </div>
<div class="mx-auto flex justify-center max-w-7xl h-full"> <div class="mx-auto flex justify-center max-w-7xl h-full">
<ul class="divide-y divide-gray-100"> <ul class="divide-y divide-gray-100">
{#each crimesList as item, i} {#each crimesList as item, i}
<li> <li>
<a <div class=" flex gap-x-4">
href="/view/{$page.params.vorgang}/{item.name}?token={token}" <a
class=" flex justify-between gap-x-6 py-5" href="/view/{$page.params.vorgang}/{item.name}?token={token}"
aria-label="zum 3D-modell" class=" flex justify-between gap-x-6 py-5"
> aria-label="zum 3D-modell"
<div class=" flex gap-x-4"> >
<Cube /> <Cube />
<div class="min-w-0 flex-auto"> </a>
{#if data?.user?.admin} <div class="min-w-0 flex-auto">
<span {#if admin}
id="label__{item.name}" <EditableItem
class="text-sm font-semibold leading-6 text-gray-900 inline-block min-w-1" list={crimesList}
contenteditable={!item.show_button} bind:editedName={names[i]}
role="textbox" currentName={item.name}
tabindex="0" onSave={handleSave}
aria-label="Dateiname bearbeiten" onDelete={handleDelete}
on:focusout={() => { ></EditableItem>
defocus_element(i); {:else}
}} <span class="text-sm font-semibold leading-6 text-gray-900 inline-block min-w-1"
on:keydown|stopPropagation={// event needed to identify ID >{item.name}</span
// TO-DO: check if event is needed or if index is sufficient >
async (ev) => { {/if}
handle_input(ev, i); {#if item.size}
}}>{item.name}</span
>
{#if item.show_button}
<button
style="padding: 2px"
id="edit__{item.name}"
aria-label="Datei umbenennen"
on:click|preventDefault={(ev) => {
let text_field_id = `label__${item.name}`;
let text_field = document.getElementById(text_field_id);
if (text_field) {
text_field.setAttribute('contenteditable', 'true');
text_field.focus();
text_field.textContent = '';
}
// hide button
item.show_button = false;
}}
>
<Edit />
</button>
{/if}
<button
style="padding: 2px"
id="del__{item.name}"
on:click|preventDefault={async (ev) => {
let delete_item = window.confirm('Bist du sicher?');
if (delete_item) {
// bucket: tatort, name: <vorgang>/item-name
let vorgang = $page.params.vorgang;
let filename = '';
if (ev && ev.currentTarget && (ev.currentTarget as HTMLElement).id) {
filename = (ev.currentTarget as HTMLElement).id.split('del__')[1];
}
// delete request
// --------------
let url = new URL($page.url);
url.pathname += `/${filename}`;
try {
const response = await fetch(`/api${url.pathname}`, { method: 'DELETE' });
if (response.status == 204) {
setTimeout(() => {
window.location.reload();
}, 500);
}
} catch (error) {
if (error instanceof Error) {
console.log(error.message);
} else {
console.log(error);
}
}
}
}}
aria-label="Datei löschen"
>
<Trash />
</button>
{:else}
<span class="text-sm font-semibold leading-6 text-gray-900 inline-block min-w-1"
>{item.name}</span
>
{/if}
<p class="mt-1 truncate text-xs leading-5 text-gray-500"> <p class="mt-1 truncate text-xs leading-5 text-gray-500">
{shortenFileSize(item.size)} {shortenFileSize(item.size)}
</p> </p>
</div> {/if}
</div> </div>
<div class="hidden sm:flex sm:flex-col sm:items-end"> <div>{item.name}</div>
<p class="text-sm leading-6 text-gray-900">3D Tatort</p> </div>
<div class="hidden sm:flex sm:flex-col sm:items-end">
<p class="text-sm leading-6 text-gray-900">3D Tatort</p>
{#if item.lastModified}
<p class="mt-1 text-xs leading-5 text-gray-500"> <p class="mt-1 text-xs leading-5 text-gray-500">
Zuletzt geändert <time datetime="2023-01-23T13:23Z" Zuletzt geändert <time datetime="2023-01-23T13:23Z"
>{timeElapsed(new Date(item.lastModified))}</time >{timeElapsed(new Date(item.lastModified))}</time
> >
</p> </p>
</div> {/if}
</a> </div>
</li> </li>
{/each} {/each}
</ul> </ul>

View File

@@ -1,24 +1,92 @@
import { client } from '$lib/minio'; import { client } from '$lib/minio';
import type { RequestHandler } from '@sveltejs/kit';
import { json } from '@sveltejs/kit';
export async function DELETE({ params }) {
const vorgang = params.vorgang;
const object_list = await new Promise((resolve, reject) => {
const res = [];
const items_str = client.listObjects('tatort', vorgang, true);
// rename operation
export async function PUT({ request }: {request: Request}) {
const data = await request.json();
// Vorgang
const vorgang = request.url.split('/').at(-1);
// prepare copy, incl. check if new name exists already
const old_name = data["old_name"];
const src_full_path = `/tatort/${vorgang}/${old_name}`;
const new_name = `${vorgang}/${data["new_name"]}`;
try {
await client.statObject('tatort', new_name);
return json({ msg: 'Die Datei existiert bereits.' }, { status: 400 });
} catch (error) {
// continue operation
console.log(error, 'continue operation');
}
// actual copy operation
await client.copyObject('tatort', new_name, src_full_path)
// delete
await client.removeObject('tatort', `${vorgang}/${old_name}`)
// return success or failure
return json({ success: 'success' }, { status: 200 });
};
/* export const PUT: RequestHandler = async ({ request, params }) => {
const { filename } = params;
const { oldName, newName } = await request.json();
if (!newName || !newName.trim()) {
return new Response('Ungültiger Name', { status: 400 });
}
const index = mockList.findIndex((name) => name === oldName);
if (index === -1) {
return new Response('Name nicht gefunden', { status: 404 });
}
if (mockList.includes(newName)) {
return new Response('Name existiert bereits', { status: 409 });
}
console.log('📥 PUT-Request empfangen:', mockList);
mockList[index] = newName;
console.log('📄 Datei:', filename);
console.log('🔁 Umbenennen:', oldName, '→', newName, mockList);
// return new Response(JSON.stringify({ success: true }), { status: 200 });
return json({ success: true, updated: newName });
};
*/
export const DELETE: RequestHandler = async ({ request })=> { //body: {request}, keine params // params= de?param1=value&params2
// const vorgang = params.vorgang;
const { vorgang } = await request.json();
const object_list:string[] = await new Promise((resolve, reject) => {
const res: string[] = [];
const items_str = client.listObjects('tatort', vorgang, true);
items_str.on('data', (obj) => { items_str.on('data', (obj) => {
res.push(obj.name); if(obj.name) res.push(obj.name);
}); });
items_str.on('error', reject); items_str.on('error', reject);
items_str.on('end', async () => { items_str.on('end', async () => {
resolve(res); resolve(res);
}); });
}); });
await client.removeObjects('tatort', object_list); await client.removeObjects('tatort', object_list);
return new Response(null, { status: 204 }); return new Response(null, { status: 204 });
} }

View File

@@ -1,4 +1,5 @@
import { BUCKET, client } from '$lib/minio'; import { BUCKET, client } from '$lib/minio';
import type { RequestHandler } from '@sveltejs/kit';
export async function GET() { export async function GET() {
const stream = client.listObjectsV2(BUCKET, '', true); const stream = client.listObjectsV2(BUCKET, '', true);
@@ -23,13 +24,13 @@ export async function GET() {
}); });
} }
export const DELETE: RequestHandler = async ({ request })=> { //body: {request}, keine params // params= de?param1=value&params2
const url_fragments = request.url.split('/');
const item = url_fragments.at(-1);
const vorgang = url_fragments.at(-2);
export async function DELETE({ request }: { request: Request }) { await client.removeObject(BUCKET, `${vorgang}/${item}`);
const url_fragments = request.url.split('/'); return new Response(null, { status: 204 });
const item = url_fragments.at(-1);
const vorgang = url_fragments.at(-2);
await client.removeObject(BUCKET, `${vorgang}/${item}`);
return new Response(null, { status: 204 });
} }