erstellt EditableItem mit frontend-error-handling eingefügt caselist, crimelist

This commit is contained in:
2025-06-26 18:40:09 +02:00
parent f8234c0488
commit f87b106ad2
5 changed files with 196 additions and 213 deletions

View File

@@ -0,0 +1,123 @@
<script lang="ts">
import Edit from '$lib/icons/Edit.svelte';
import Trash from '$lib/icons/Trash.svelte';
import { createEventDispatcher } from 'svelte';
import { validateInput } from '$lib/helper/error-utils';
interface ListItem {
name: string;
token?: string;
// add other properties as needed
}
export let value: string = '';
export let variant: '' | 'casename' | 'crimename' = ''; // casename | crimename
export let existings: ListItem[];
export let id: number;
export let editable: boolean = true;
export let editing: boolean;
console.log('Debug editing', editing);
const existingNames = existings.map((item) => item.name);
const dispatch = createEventDispatcher<{
editSart: {};
save: {};
delete: {};
cancel: void;
}>();
let internalValue = value;
let oldValue = value;
let showWarning = false;
let duplicate = false;
let errors: string[] = [];
let errorText = '';
$: errors = validateInput(oldValue, internalValue, { minLength: 3, existingNames });
function startEdit() {
oldValue = value;
internalValue = value;
editing = true;
dispatch('editSart', { id, value, variant, editing });
}
function cancelEdit() {
internalValue = oldValue;
editing = false;
showWarning = false;
console.log('Abgebrochen');
dispatch('cancel');
}
function saveEdit(ev?: MouseEvent | KeyboardEvent) {
ev?.preventDefault();
ev?.stopPropagation();
const name = internalValue.trim();
if (errors.length !== 0) {
showWarning = true;
console.log('Abgebrochen', errors);
return;
}
if (!name) {
showWarning = true;
console.log('Abgebrochen', showWarning);
return;
}
if (existingNames.includes(name) && name !== oldValue.trim()) {
showWarning = true;
console.log('Abgebrochen', showWarning);
return;
}
editing = false;
showWarning = false;
duplicate = false;
dispatch('save', { newValue: internalValue, oldValue, variant });
}
function deleteItem() {
dispatch('delete', { value, variant });
}
function handleKey(e: KeyboardEvent) {
if (e.key === 'Enter') saveEdit(e);
if (e.key === 'Escape') cancelEdit();
}
</script>
<div class="" {...$$restProps}>
<div class="flex gap-x-2">
{#if editing}
<input
bind:value={internalValue}
on:keydown={handleKey}
on:blur|preventDefault|stopPropagation={cancelEdit}
class=""
class:bg-red-200={(showWarning && editing) || errors.length !== 0}
/>
{:else}
<span>{value}</span>
{#if !editing && editable}
<button on:click|preventDefault|stopPropagation={startEdit}>
<Edit />
</button>
<button on:click|preventDefault={deleteItem}>
<Trash />
</button>
{/if}
{/if}
</div>
{#if editing && errors}
<p class="text-red-600 text-sm mt-1 font-medium">{errors[0]}</p>
{/if}
</div>

View File

@@ -1,85 +0,0 @@
<script lang="ts">
import Cube from '$lib/icons/Cube.svelte';
import Edit from '$lib/icons/Edit.svelte';
import Trash from '$lib/icons/Trash.svelte';
import shortenFileSize from '$lib/helper/shortenFileSize';
import timeElapsed from '$lib/helper/timeElapsed';
export let data;
export let i;
let crimesList = data.crimesList;
let item = crimesList[i];
let admin = data.user?.admin;
let link = `/view/${item.prefix}/${item.name}?token=${data.caseToken}`;
console.log('Debug Mina', crimesList, data);
function editName() {
console.log('Edit Name');
}
function deleteCase() {
console.log('Delete Case');
}
function checkIsEmpty() {
if (!item.name) console.log('Das Feld ist leer');
}
function cancelEdit() {
console.log('Bearbeitung abgebrochen');
}
</script>
<a href={link} class="flex justify-between items-center gap-x-6 py-5">
<div class=" flex gap-x-4">
<Cube class="h-auto" />
<div class="min-w-0 flex-auto">
{#if admin}
<input
class="text-sm font-semibold leading-6 text-gray-900 inline-block min-w-1"
type="text"
value={item.name}
on:click|preventDefault={(ev) => {
editName();
}}
/>
<button
class="p-2"
on:click|preventDefault={(ev) => {
editName();
}}
>
<Edit />
</button>
<button
class="p-2"
on:click|preventDefault={async (ev) => {
deleteCase();
}}
>
<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">{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
>
</p>
</div>
</a>

View File

@@ -0,0 +1,9 @@
export function validateInput(oldValue:string, value: string, options: { minLength?: number; existingNames?: string[] }) {
const errors: string[] = [];
if (!value.trim()) errors.push('Feld darf nicht leer sein');
if (options.existingNames?.includes(value) && oldValue !== value)
errors.push('Name existiert bereits');
return errors;
}

View File

@@ -1,11 +1,17 @@
<script lang="ts">
import Trash from '$lib/icons/Trash.svelte';
import Folder from '$lib/icons/Folder.svelte';
import type { PageData } from '../$types';
import EditableItem from '$lib/components/EditableItem.svelte';
export let data: PageData;
export let data;
export let editingId: number;
const caseList = data.caseList;
interface ListItem {
name: string;
token?: string;
// add other properties as needed
}
const caseList: ListItem[] = data.caseList;
async function delete_item(ev: Event) {
let delete_item = window.confirm('Bist du sicher?');
@@ -14,7 +20,7 @@
const target = ev.currentTarget as HTMLElement | null;
if (!target) return;
let filename = target.id.split('del__')[1];
// delete request
// --------------
@@ -44,27 +50,31 @@
</div>
<div class="mx-auto flex justify-center max-w-7xl h-full">
<ul role="list" class="divide-y divide-gray-100">
{#each caseList as item}
{#each caseList as item, i}
<li>
<a href="/list/{item.name}?token={item.token}" class="flex justify-between gap-x-6 py-5">
<div class="flex gap-x-4">
<!-- Ordner -->
<Folder />
<div class="min-w-0 flex-auto">
<span class="text-sm font-semibold leading-6 text-gray-900">{item.name}</span>
<!-- Delete button -->
<button
style="padding: 2px"
id="del__{item.name}"
on:click|preventDefault={delete_item}
aria-label="Vorgang {item.name} löschen"
>
<Trash />
</button>
<EditableItem
class=""
id={i}
value={item.name}
editing={editingId === i}
on:editStart={() => (editingId = i)}
variant="casename"
existings={caseList}
on:save={(e) => console.log('Gespeichert:', e.detail)}
on:delete={(e) => {
console.log('Gelöscht:', e.detail);
delete_item(e);
}}
></EditableItem>
</div>
<div class="hidden sm:flex sm:flex-col sm:items-end">
<p class="text-sm leading-6 text-gray-900">Vorgang</p>
</div>
</div>
<div class="hidden sm:flex sm:flex-col sm:items-end">
<p class="text-sm leading-6 text-gray-900">Vorgang</p>
</div>
</a>
</li>

View File

@@ -6,14 +6,17 @@
import ModalTitle from '$lib/components/Modal/ModalTitle.svelte';
import ModalContent from '$lib/components/Modal/ModalContent.svelte';
import ModalFooter from '$lib/components/Modal/ModalFooter.svelte';
import ListItem from '$lib/components/ListItem.svelte';
import EditableItem from '$lib/components/EditableItem.svelte';
import Cube from '$lib/icons/Cube.svelte';
import shortenFileSize from '$lib/helper/shortenFileSize.js';
import timeElapsed from '$lib/helper/timeElapsed.js';
export let data;
interface ListItem {
name: string;
size: number;
lastModified: string | number | Date;
size?: number;
lastModified?: string | number | Date | undefined;
show_button?: boolean;
// add other properties as needed
}
@@ -28,6 +31,9 @@
$: inProgress;
let err = false;
$: err;
let editingId: number;
// let admin = data?.user?.admin;
let admin = true;
let rename_input;
$: rename_input;
@@ -36,21 +42,6 @@
open = false;
}
// 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 handle_input(ev: KeyboardEvent, i: number) {
let item = crimesList[i];
if (ev.key == 'Escape') {
@@ -131,7 +122,7 @@
<ul class="divide-y divide-gray-100">
{#each crimesList as item, i}
<li>
<!-- <a
<a
href="/view/{$page.params.vorgang}/{item.name}?token={token}"
class=" flex justify-between gap-x-6 py-5"
aria-label="zum 3D-modell"
@@ -139,106 +130,41 @@
<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(i);
}}
on:keydown|stopPropagation={// event needed to identify ID
// TO-DO: check if event is needed or if index is sufficient
async (ev) => {
handle_input(ev, i);
}}>{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}`, { 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>
{#if admin}
<EditableItem
class="bg-lred-500"
id={i}
value={item.name}
editing={editingId === i}
on:editStart={() => (editingId = i)}
variant="crimename"
existings={crimesList}
on:save={(e) => console.log('Gespeichert:', e.detail)}
on:delete={(e) => console.log('Gelöscht:', e.detail)}
></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> -->
<ListItem {data} {i}></ListItem>
</a>
</li>
{/each}
</ul>