f047_neu_Edit-der-Namen #28

Merged
trachi93 merged 14 commits from f047_neu_Edit-der-Namen into development 2025-08-19 09:30:13 +02:00
4 changed files with 456 additions and 228 deletions
Showing only changes of commit a91d7292c5 - Show all commits

View File

@@ -0,0 +1,93 @@
<script lang="ts">
Outdated
Review

Die Componente heißt EditableItem. Das suggeriert, dass damit unterschieldiche Items editierbar sind, also generisch. In meinem Verständins ist das aber eine Componente zugeschnitten für das Editieren des Namens

Die Componente heißt EditableItem. Das suggeriert, dass damit unterschieldiche Items editierbar sind, also generisch. In meinem Verständins ist das aber eine Componente zugeschnitten für das Editieren des Namens
Outdated
Review

Habe es umbenannt in NameItemEditor, falls besserer Name bitte eigenständig umbenennen, es wird nur einmal verwendet.

Habe es umbenannt in NameItemEditor, falls besserer Name bitte eigenständig umbenennen, es wird nur einmal verwendet.
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);
let error: string = $derived(validateName(localName));
mina marked this conversation as resolved Outdated
Outdated
Review

manualError wird m. E. nicht im code gesetzt. Also es wird nie ein Error definiert, oder?

manualError wird m. E. nicht im code gesetzt. Also es wird nie ein Error definiert, oder?
Outdated
Review

Habe ich rausgenommen

Habe ich rausgenommen
let manualError = $state('');
let isEditing = $state(false);
let inputRef: HTMLInputElement;
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 '';
}
function commitIfValid() {
if (!error && !wasCancelled && localName != currentName) {
Outdated
Review

Die Funktion commitIfValid setzt editedName = localName.trim();, aber editedName ist ein Prop und sollte nicht direkt überschrieben werden. Stattdessen sollte ein Event ausgelöst werden oder ein Callback genutzt werden.

Die Funktion commitIfValid setzt editedName = localName.trim();, aber editedName ist ein Prop und sollte nicht direkt überschrieben werden. Stattdessen sollte ein Event ausgelöst werden oder ein Callback genutzt werden.
Outdated
Review

Ich habe es nun eine neue Variable und dann in onSave übergeben, dabei ist mir die Frage gekommen wie sieht die Abfrage mit Leerzeichen insgesamt aus, habe es als offene Frage ins Backlog geschrieben

Ich habe es nun eine neue Variable und dann in onSave übergeben, dabei ist mir die Frage gekommen wie sieht die Abfrage mit Leerzeichen insgesamt aus, habe es als offene Frage ins Backlog geschrieben
editedName = localName.trim();
onSave(editedName, currentName);
} else {
localName = currentName;
resetEdit();
}
}
function resetEdit() {
wasCancelled = false;
manualError = '';
inputRef?.blur();
isEditing = false;
}
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

@@ -1,17 +1,28 @@
import { getVorgangByToken, getCrimesListByToken } from '$lib/server/vorgangService';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params, url }) => {
export const load: PageServerLoad = async ({fetch, params, url }) => {
const vorgangToken = params.vorgang;
const vorgangPIN = url.searchParams.get('pin');
const adminRes = await fetch(`/api/user`)
const user = await adminRes.json()
const crimesList = await getCrimesListByToken(vorgangToken); // TatortList zum Vorgang
const vorgang = getVorgangByToken(vorgangToken); //einzelner Vorgang
const vorgangObjekt = getVorgangByToken(vorgangToken); //einzelner Vorgang //TypeScript darf nicht undefined sein
let vorgangName:string;
if(vorgangObjekt){
vorgangName = vorgangObjekt.name;
}else{
vorgangName = '';
}
return {
crimesList,
vorgangPIN,
vorgang
crimesList, //crimesList
vorgangPIN, //caseToken
vorgangName, //caseId?
vorgangObjekt,
user
};
};

View File

@@ -1,7 +1,5 @@
<script lang="ts">
Review

Wie du beschrieben hast, unnötige Kommentare und console.logs raus

Wie du beschrieben hast, unnötige Kommentare und console.logs raus
Review

Sollte soweit passen, ansonsten bitte selbstständig rauslöschen

Sollte soweit passen, ansonsten bitte selbstständig rauslöschen
import shortenFileSize from '$lib/helper/shortenFileSize';
import { page } from '$app/stores';
import timeElapsed from '$lib/helper/timeElapsed';
import Alert from '$lib/components/Alert.svelte';
@@ -11,14 +9,16 @@
import ModalContent from '$lib/components/Modal/ModalContent.svelte';
import ModalFooter from '$lib/components/Modal/ModalFooter.svelte';
import Cube from '$lib/icons/Cube.svelte';
import Edit from '$lib/icons/Edit.svelte';
import Trash from '$lib/icons/Trash.svelte';
/** export let data; */
/** @type {import('./$types').PageData} */
export let data;
import EditableItem from '$lib/components/EditableItem.svelte';
console.log(data);
import { invalidate } from '$app/navigation';
import { page } from '$app/stores';
//Seite für die Tatort-Liste
let { data } = $props();
console.log('tatorte: debug ', data);
interface ListItem {
name: string;
@@ -28,264 +28,378 @@
// add other properties as needed
}
const vorgang = data.vorgang;
const crimesList: ListItem[] = data.crimesList;
const vorgangPIN: string = data.vorgangPIN;
let open = false;
$: open;
let inProgress = false;
$: inProgress;
let err = false;
$: err;
let rename_input;
$: rename_input;
let vorgang: string = data.vorgangName; // let vorgang = data.caseId; <!--caseId, vorgang.name aber das funktioniert nicht richtig, daher in server.ts geändert-->
let crimesList: ListItem[] = $state(data.crimesList);
const vorgangPIN: string = data.vorgangPIN; //caseToken?? // const token: string | null = data.caseToken;
//Variablen für Modal
let open = $state(false);
let inProgress = $state(false);
mina marked this conversation as resolved Outdated
Outdated
Review

In Progress wird nie gesetzt. Daher bisher keine Funktion

In Progress wird nie gesetzt. Daher bisher keine Funktion
Outdated
Review

Da es zum Modal gehört habe ich es angepasst und nicht gelöscht.

Da es zum Modal gehört habe ich es angepasst und nicht gelöscht.
let err = $state(false);
function uploadSuccessful() {
open = false;
}
function defocus_element(i: number) {
let item = crimesList[i];
let text_field_id = `label__${item.name}`;
//Variabeln für EditableItem
let names: string[] = $state(crimesList.map((l) => l.name));
let editedName: string = $state('');
let currentName: string = $state('');
let text_field = document.getElementById(text_field_id);
if (text_field) {
text_field.setAttribute('contenteditable', 'false');
text_field.textContent = item.name;
}
let editingId: number;
mina marked this conversation as resolved Outdated
Outdated
Review

Funktioniert es denn jetzt???

Funktioniert es denn jetzt???
Outdated
Review

Ja, funktioniert.

Ja, funktioniert.
// reshow button
crimesList[i].show_button = true;
return;
}
// let admin = data?.user?.admin;
let admin = true;
async function handleEditFieldInput(ev: KeyboardEvent, listItemIndex: number) {
let item = crimesList[listItemIndex];
if (ev.key == 'Escape') {
let text_field_id = `label__${item.name}`;
let rename_input;
let text_field = document.getElementById(text_field_id);
if (text_field) {
text_field.setAttribute('contenteditable', 'false');
text_field.textContent = item.name;
}
$effect(() => {
console.log('rename_input hat sich geändert:', rename_input);
});
// reshow button
item.show_button = true;
return;
}
if (ev.key == 'Enter') {
let name_field = ev.currentTarget as HTMLElement | null;
let new_name = name_field
? name_field.textContent || (name_field as any).innerText || ''
: '';
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({ vorgang, oldName, newName })
});
const clone = res.clone();
const data = await res.json();
let message = data.message;
let error = !data.success;
if (new_name == '') {
alert('Bitte einen gültigen Namen eingeben.');
ev.preventDefault();
return;
}
console.log('Tatort Update: ', message);
// actual upload
// -------------
// to prevent from item being selected
ev.preventDefault();
// construct PUT URL
const url = $page.url.pathname.split('?')[0];
let data_obj: { new_name: string; old_name: string } = { new_name: '', old_name: '' };
data_obj['new_name'] = new_name;
data_obj['old_name'] =
ev.currentTarget && (ev.currentTarget as HTMLElement).id
? (ev.currentTarget as HTMLElement).id.split('__')[1]
: '';
open = true;
inProgress = true;
const response = await fetch(url, { method: 'PUT', body: JSON.stringify(data_obj) });
inProgress = false;
if (!response.ok) {
err = true;
if (response.status == 400) {
let json_res = await response.json();
return;
}
throw new Error(`Fehlgeschlagen: ${response.status}`);
if (!res.ok) {
const msg = await clone.text();
console.error('❌ Fehler beim Speichern:', msg);
} else {
uploadSuccessful();
setTimeout(() => {
window.location.reload();
}, 500);
console.log('✅ Erfolgreich gespeichert:', newName);
await invalidate('');
currentName = newName;
}
// --- upload finished ---
return;
} catch (err) {
console.error('⚠️ Netzwerkfehler:', err);
}
}
function constructMailToLink() {
const subject = 'Link zum Tatvorgang';
const link = $page.url.toString().split('?')[0];
const body = `Hallo,
async function handleDelete(tatort: string) {
let url = new URL($page.url);
url.pathname += `/${tatort}`;
console.log('Delete tatort: ', `/api${url.pathname}`, url.pathname);
hier ist der Link zum Tatvorgang:
${link}
try {
const res = await fetch(`/api${url.pathname}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ vorgang, tatort })
});
Der Zugangs-PIN wird zur Sicherheit über einen zweiten Kommunikationskanal übermittelt.
Mit freundlichen Grüßen,
`;
const mailtoLink = `mailto:?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
return mailtoLink;
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);
}
}
// function defocus_element(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 handleEditFieldInput(ev: KeyboardEvent, listItemIndex: number) {
// let item = crimesList[listItemIndex];
// if (ev.key == 'Escape') {
// 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
// item.show_button = true;
// return;
// }
// if (ev.key == 'Enter') {
// let name_field = ev.currentTarget as HTMLElement | null;
// let new_name = name_field
// ? name_field.textContent || (name_field as any).innerText || ''
// : '';
// if (new_name == '') {
// alert('Bitte einen gültigen Namen eingeben.');
// ev.preventDefault();
// return;
// }
// // actual upload
// // -------------
// // to prevent from item being selected
// ev.preventDefault();
// // construct PUT URL
// const url = $page.url.pathname.split('?')[0];
// let data_obj: { new_name: string; old_name: string } = { new_name: '', old_name: '' };
// data_obj['new_name'] = new_name;
// data_obj['old_name'] =
// ev.currentTarget && (ev.currentTarget as HTMLElement).id
// ? (ev.currentTarget as HTMLElement).id.split('__')[1]
// : '';
// open = true;
// inProgress = true;
// const response = await fetch(url, { method: 'PUT', body: JSON.stringify(data_obj) });
// inProgress = false;
// if (!response.ok) {
// err = true;
// if (response.status == 400) {
// let json_res = await response.json();
// return;
// }
// throw new Error(`Fehlgeschlagen: ${response.status}`);
// } else {
// uploadSuccessful();
// setTimeout(() => {
// window.location.reload();
// }, 500);
// }
// // --- upload finished ---
// return;
// }
// }
// function constructMailToLink() {
// const subject = 'Link zum Tatvorgang';
// const link = $page.url.toString().split('?')[0];
// const body = `Hallo,
// hier ist der Link zum Tatvorgang:
// ${link}
// Der Zugangs-PIN wird zur Sicherheit über einen zweiten Kommunikationskanal übermittelt.
// Mit freundlichen Grüßen,
// `;
// const mailtoLink = `mailto:?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
// return mailtoLink;
// }
</script>
<div class="-z-10 bg-white">
<div class="flex flex-col items-center justify-center w-full">
<h1 class="text-xl">Vorgang {vorgang.name}</h1>
{#if data?.user?.admin}
Zugangs-PIN: {vorgang.pin}
<a href={constructMailToLink()}><Button>Share Link</Button></a>
{/if}
</div>
<div class="mx-auto flex justify-center max-w-7xl h-full">
<ul class="divide-y divide-gray-100">
{#each crimesList as item, crimeListItemIndex}
<li>
<a
href="/view/{$page.params.vorgang}/{item.name}?pin={vorgangPIN}"
class=" flex justify-between gap-x-6 py-5"
aria-label="zum 3D-modell"
>
<div class=" flex gap-x-4">
<Cube />
<div class="min-w-0 flex-auto">
{#if data?.user?.admin}
<span
id="label__{item.name}"
class="text-sm font-semibold leading-6 text-gray-900 inline-block min-w-1"
contenteditable={!item.show_button}
role="textbox"
tabindex="0"
aria-label="Dateiname bearbeiten"
on:focusout={() => {
defocus_element(crimeListItemIndex);
}}
on:keydown|stopPropagation={// event needed to identify ID
// TO-DO: check if event is needed or if index is sufficient
async (ev) => {
handleEditFieldInput(ev, crimeListItemIndex);
}}>{item.name}</span
>
{#if vorgang}
<div class="-z-10 bg-white">
<div class="flex flex-col items-center justify-center w-full">
<h1 class="text-xl">Vorgang {vorgang}</h1>
{#if item.show_button}
<!-- {#if data?.user?.admin}
Zugangs-PIN: {vorgang.pin}
<a href={constructMailToLink()}><Button>Share Link</Button></a>
{/if} -->
</div>
<div class="mx-auto flex justify-center max-w-7xl h-full">
<ul class="divide-y divide-gray-100">
{#each crimesList as item, crimeListItemIndex}
<!-- <li>
<a
href="/view/{$page.params.vorgang}/{item.name}?pin={vorgangPIN}"
class=" flex justify-between gap-x-6 py-5"
aria-label="zum 3D-modell"
>
<div class=" flex gap-x-4">
<Cube />
<div class="min-w-0 flex-auto">
{#if data?.user?.admin}
<span
id="label__{item.name}"
class="text-sm font-semibold leading-6 text-gray-900 inline-block min-w-1"
contenteditable={!item.show_button}
role="textbox"
tabindex="0"
aria-label="Dateiname bearbeiten"
on:focusout={() => {
defocus_element(crimeListItemIndex);
}}
on:keydown|stopPropagation={// event needed to identify ID
// TO-DO: check if event is needed or if index is sufficient
async (ev) => {
handleEditFieldInput(ev, crimeListItemIndex);
}}>{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="edit__{item.name}"
aria-label="Datei umbenennen"
on:click|preventDefault={(ev) => {
let text_field_id = `label__${item.name}`;
id="del__{item.name}"
on:click|preventDefault={async (ev) => {
let delete_item = window.confirm('Bist du sicher?');
let text_field = document.getElementById(text_field_id);
if (text_field) {
text_field.setAttribute('contenteditable', 'true');
text_field.focus();
text_field.textContent = '';
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);
}
}
}
// hide button
item.show_button = false;
}}
aria-label="Datei löschen"
>
<Edit />
<Trash />
</button>
{:else}
<span class="text-sm font-semibold leading-6 text-gray-900 inline-block min-w-1"
>{item.name}</span
>
{/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"
<p class="mt-1 truncate text-xs leading-5 text-gray-500">
{shortenFileSize(item.size)}
</p>
</div>
</div>
<div class="hidden sm:flex sm:flex-col sm:items-end">
<p class="text-sm leading-6 text-gray-900">3D Tatort</p>
<p class="mt-1 text-xs leading-5 text-gray-500">
Zuletzt geändert <time datetime="2023-01-23T13:23Z"
>{timeElapsed(new Date(item.lastModified))}</time
>
<Trash />
</button>
</p>
</div>
</a>
</li> -->
<li>
<div class=" flex gap-x-4">
<a
href="/view/{$page.params.vorgang}/{currentName}?pin={vorgangPIN}"
class=" flex justify-between gap-x-6 py-5"
aria-label="zum 3D-modell"
>
<Cube />
</a>
<div class="min-w-0 flex-auto">
{#if admin}
<EditableItem
list={crimesList}
bind:editedName={names[crimeListItemIndex]}
currentName={item.name}
onSave={handleSave}
onDelete={handleDelete}
></EditableItem>
{: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">
{shortenFileSize(item.size)}
</p>
{#if item.size}
<p class="mt-1 truncate text-xs leading-5 text-gray-500">
{shortenFileSize(item.size)}
</p>
{/if}
</div>
</div>
<div class="hidden sm:flex sm:flex-col sm:items-end">
<p class="text-sm leading-6 text-gray-900">3D Tatort</p>
<p class="mt-1 text-xs leading-5 text-gray-500">
Zuletzt geändert <time datetime="2023-01-23T13:23Z"
>{timeElapsed(new Date(item.lastModified))}</time
>
</p>
{#if item.lastModified}
<p class="mt-1 text-xs leading-5 text-gray-500">
Zuletzt geändert <time datetime="2023-01-23T13:23Z"
>{timeElapsed(new Date(item.lastModified))}</time
>
</p>
{/if}
</div>
</a>
</li>
{/each}
</ul>
</div>
</li>
{/each}
</ul>
</div>
<Modal {open}
><ModalTitle>Umbenennen</ModalTitle><ModalContent>
{#if inProgress}
<p class="py-2 mb-1">Vorgang läuft...</p>
{/if}
{#if err}
<Alert class="w-full" type="error">Fehler beim Umbenennen</Alert>
{/if}
</ModalContent>
<ModalFooter><Button disabled={inProgress} on:click={uploadSuccessful}>Ok</Button></ModalFooter>
</Modal>
</div>
<Modal {open}
><ModalTitle>Umbenennen</ModalTitle><ModalContent>
{#if inProgress}
<p class="py-2 mb-1">Vorgang läuft...</p>
{/if}
{#if err}
<Alert class="w-full" type="error">Fehler beim Umbenennen</Alert>
{/if}
</ModalContent>
<ModalFooter
><Button disabled={inProgress} on:click={uploadSuccessful}>Ok</Button></ModalFooter
>
</Modal>
</div>
{/if}
<style>
ul {

View File

@@ -0,0 +1,10 @@
//Rollenabfrage ob Benutzer admin ist
import { json } from "@sveltejs/kit";
export async function GET({locals}) {
const isAdmin = locals.user?.admin === true;
const data = {admin: isAdmin}
return json(data)
}