f047_Edit-der-Namen #15
123
src/lib/components/EditableItem.svelte
Normal 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
|
||||
|
mina marked this conversation as resolved
Outdated
|
||||
export let existings: ListItem[];
|
||||
export let id: number;
|
||||
export let editable: boolean = true;
|
||||
export let editing: boolean;
|
||||
|
||||
console.log('Debug editing', editing);
|
||||
|
mina marked this conversation as resolved
Outdated
jared
commented
Bitte Console.log entfernen. Bitte Console.log entfernen.
|
||||
|
||||
const existingNames = existings.map((item) => item.name);
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
editSart: {};
|
||||
|
mina marked this conversation as resolved
Outdated
jared
commented
Typo editSart to editStart Typo editSart to editS**t**art
|
||||
save: {};
|
||||
delete: {};
|
||||
cancel: void;
|
||||
}>();
|
||||
|
||||
|
jared marked this conversation as resolved
Outdated
jared
commented
The signature '(): EventDispatcher<{ editSart: {}; save: {}; delete: {}; cancel: void; }>' of 'createEventDispatcher' is deprecated. Mittlerweile wird ein Event mittels Callback function dispatched. Gucke ggf. noch einmal in die Docs. Zum Beispiel: <script type="ts"> const props: { onClick(): void; onExplode(name: string, age: number): void; } = $props(); </script><MyComponent The signature '(): EventDispatcher<{ editSart: {}; save: {}; delete: {}; cancel: void; }>' of 'createEventDispatcher' is deprecated.
Mittlerweile wird ein Event mittels Callback function dispatched. Gucke ggf. noch einmal in die Docs.
Zum Beispiel:
<script type="ts">
const props: {
onClick(): void;
onExplode(name: string, age: number): void;
} = $props();
</script>
<MyComponent
onClick={() => alert('clicked')}
onExplode={(name, age) => alert(name + ' ' + age)}
/>
mina
commented
Ich habe es angepasst, es musste einiges verändert werden, bitte schaue es dir nochmal an. Ich habe es angepasst, es musste einiges verändert werden, bitte schaue es dir nochmal an.
|
||||
let internalValue = value;
|
||||
|
jared marked this conversation as resolved
Outdated
jared
commented
Villt die Variable noch besser bennennen. InternalValue oder oldValue, kann halt alles sein... Villt die Variable noch besser bennennen. InternalValue oder oldValue, kann halt alles sein...
mina
commented
Allgemein, weil es eine Komponente ist, die vielseitig einsetzbar. Es geht darum quasi den Startwert? zu speichern, für den Abbruch und den aktuellen Wert, um ein editieren in Gang zu bringen. Man könnte es auch cancelValue und EditValue nennen? Vielleicht besser? Allgemein, weil es eine Komponente ist, die vielseitig einsetzbar. Es geht darum quasi den Startwert? zu speichern, für den Abbruch und den aktuellen Wert, um ein editieren in Gang zu bringen. Man könnte es auch cancelValue und EditValue nennen? Vielleicht besser?
jared
commented
Naja, der Begriff Value bringt halt nicht, weil das quasi immer ein value ist. Daher so explizit bennen wie es geht Naja, der Begriff Value bringt halt nicht, weil das quasi immer ein value ist. Daher so explizit bennen wie es geht
|
||||
let oldValue = value;
|
||||
let showWarning = false;
|
||||
let duplicate = false;
|
||||
|
||||
let errors: string[] = [];
|
||||
let errorText = '';
|
||||
|
mina marked this conversation as resolved
Outdated
jared
commented
Wird im weiteren Verlauf nicht verwendet, daher löschen Wird im weiteren Verlauf nicht verwendet, daher löschen
|
||||
|
||||
$: 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>
|
||||
@@ -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>
|
||||
9
src/lib/helper/error-utils.ts
Normal 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;
|
||||
}
|
||||
|
jared
commented
Grundsätzlich wird hier der Value in diesem Fall ein Name validiert. Es ist keine generische validateInput Funktion. Daher sollte sie entsprechend benannt werden. Darüber hinaus wird sie nur in EditableItem verwendet. Daher ist ggf. ein Auslagern nicht sinnvoll, oder soll sie noch wo anders verwendet werden? Grundsätzlich wird hier der Value in diesem Fall ein Name validiert. Es ist keine generische validateInput Funktion. Daher sollte sie entsprechend benannt werden. Darüber hinaus wird sie nur in EditableItem verwendet. Daher ist ggf. ein Auslagern nicht sinnvoll, oder soll sie noch wo anders verwendet werden?
mina
commented
Werde ich nochmal überarbeiten Werde ich nochmal überarbeiten
|
||||
@@ -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 {
|
||||
|
jared
commented
Man könnte einen Ordner für type anlegen src/lib/types/... Man könnte einen Ordner für type anlegen src/lib/types/...
mina
commented
s.u. s.u.
|
||||
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>
|
||||
|
||||
@@ -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 {
|
||||
|
jared
commented
s. o. s. o.
mina
commented
Ja, das können wir mal im Daily ansprechen, ich habe ein Ticket daraus gemacht. Ja, das können wir mal im Daily ansprechen, ich habe ein Ticket daraus gemacht.
|
||||
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>
|
||||
|
||||
Der Kommentar könnte m.M.n. raus