mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:01:08 +02:00
feat(manacore/web): add undo toasts for delete and tag removal
Extend toast system with action buttons and toastStore.undo() helper. After deleting a task/event/contact or removing a tag, a toast with "Rückgängig" button appears for 5 seconds. Clicking it restores the item (clears deletedAt) or re-adds the tag. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a673a69972
commit
81716725f2
6 changed files with 92 additions and 18 deletions
|
|
@ -11,6 +11,7 @@
|
|||
import type { LocalEvent } from '../types';
|
||||
import { useAllTags, getTagsByIds } from '$lib/stores/tags.svelte';
|
||||
import LinkedItems from '$lib/components/links/LinkedItems.svelte';
|
||||
import { toastStore } from '@manacore/shared-ui/toast';
|
||||
|
||||
let { navigate, goBack, params }: ViewProps = $props();
|
||||
let eventId = $derived(params.eventId as string);
|
||||
|
|
@ -34,10 +35,11 @@
|
|||
|
||||
async function removeTag(tagId: string) {
|
||||
const current = event?.tagIds ?? [];
|
||||
await eventsStore.updateTagIds(
|
||||
eventId,
|
||||
current.filter((id) => id !== tagId)
|
||||
);
|
||||
const removed = current.filter((id) => id !== tagId);
|
||||
await eventsStore.updateTagIds(eventId, removed);
|
||||
toastStore.undo('Tag entfernt', () => {
|
||||
eventsStore.updateTagIds(eventId, current);
|
||||
});
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
|
|
@ -84,8 +86,12 @@
|
|||
}
|
||||
|
||||
async function deleteEvent() {
|
||||
await eventsStore.deleteEvent(eventId);
|
||||
const id = eventId;
|
||||
await eventsStore.deleteEvent(id);
|
||||
goBack();
|
||||
toastStore.undo('Termin gelöscht', () => {
|
||||
db.table('events').update(id, { deletedAt: undefined, updatedAt: new Date().toISOString() });
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
import type { LocalContact } from '../types';
|
||||
import { useAllTags, getTagsByIds } from '$lib/stores/tags.svelte';
|
||||
import LinkedItems from '$lib/components/links/LinkedItems.svelte';
|
||||
import { toastStore } from '@manacore/shared-ui/toast';
|
||||
|
||||
let { navigate, goBack, params }: ViewProps = $props();
|
||||
let contactId = $derived(params.contactId as string);
|
||||
|
|
@ -49,10 +50,11 @@
|
|||
|
||||
async function removeTag(tagId: string) {
|
||||
const current = contact?.tagIds ?? [];
|
||||
await contactsStore.updateTagIds(
|
||||
contactId,
|
||||
current.filter((id) => id !== tagId)
|
||||
);
|
||||
const removed = current.filter((id) => id !== tagId);
|
||||
await contactsStore.updateTagIds(contactId, removed);
|
||||
toastStore.undo('Tag entfernt', () => {
|
||||
contactsStore.updateTagIds(contactId, current);
|
||||
});
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
|
|
@ -119,8 +121,15 @@
|
|||
}
|
||||
|
||||
async function deleteContact() {
|
||||
await contactsStore.deleteContact(contactId);
|
||||
const id = contactId;
|
||||
await contactsStore.deleteContact(id);
|
||||
goBack();
|
||||
toastStore.undo('Kontakt gelöscht', () => {
|
||||
db.table('contacts').update(id, {
|
||||
deletedAt: undefined,
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
import type { LocalTask, TaskPriority } from '../types';
|
||||
import { useAllTags, getTagsByIds } from '$lib/stores/tags.svelte';
|
||||
import LinkedItems from '$lib/components/links/LinkedItems.svelte';
|
||||
import { toastStore } from '@manacore/shared-ui/toast';
|
||||
|
||||
let { navigate, goBack, params }: ViewProps = $props();
|
||||
let taskId = $derived(params.taskId as string);
|
||||
|
|
@ -38,10 +39,11 @@
|
|||
|
||||
async function removeTag(tagId: string) {
|
||||
const current = getTaskTagIds();
|
||||
await tasksStore.updateLabels(
|
||||
taskId,
|
||||
current.filter((id) => id !== tagId)
|
||||
);
|
||||
const removed = current.filter((id) => id !== tagId);
|
||||
await tasksStore.updateLabels(taskId, removed);
|
||||
toastStore.undo('Tag entfernt', () => {
|
||||
tasksStore.updateLabels(taskId, current);
|
||||
});
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
|
|
@ -96,8 +98,12 @@
|
|||
}
|
||||
|
||||
async function deleteTask() {
|
||||
await tasksStore.deleteTask(taskId);
|
||||
const id = taskId;
|
||||
await tasksStore.deleteTask(id);
|
||||
goBack();
|
||||
toastStore.undo('Aufgabe gelöscht', () => {
|
||||
db.table('tasks').update(id, { deletedAt: undefined, updatedAt: new Date().toISOString() });
|
||||
});
|
||||
}
|
||||
|
||||
const priorityLabels: Record<TaskPriority, string> = {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,17 @@
|
|||
<div class="toast-item {colors[toast.type]}" role="alert">
|
||||
<Icon size={20} weight="fill" class="toast-icon" />
|
||||
<p class="toast-message">{toast.message}</p>
|
||||
{#if toast.action}
|
||||
<button
|
||||
onclick={() => {
|
||||
toast.action?.onClick();
|
||||
handleDismiss(toast.id);
|
||||
}}
|
||||
class="toast-action"
|
||||
>
|
||||
{toast.action.label}
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
onclick={() => handleDismiss(toast.id)}
|
||||
class="toast-dismiss"
|
||||
|
|
@ -92,6 +103,23 @@
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
.toast-action {
|
||||
flex-shrink: 0;
|
||||
padding: 0.25rem 0.625rem;
|
||||
border-radius: 0.375rem;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
cursor: pointer;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: inherit;
|
||||
transition: all 0.15s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.toast-action:hover {
|
||||
background: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
.toast-dismiss {
|
||||
flex-shrink: 0;
|
||||
padding: 0.25rem;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
export { toastStore, toast, handleApiError } from './toast.svelte';
|
||||
export type { Toast, ToastType } from './toast.svelte';
|
||||
export type { Toast, ToastType, ToastAction } from './toast.svelte';
|
||||
export { default as ToastContainer } from './ToastContainer.svelte';
|
||||
export { setupGlobalErrorHandler, GLOBAL_ERROR_TRANSLATIONS } from './globalErrorHandler';
|
||||
export type {
|
||||
|
|
|
|||
|
|
@ -20,11 +20,17 @@
|
|||
|
||||
export type ToastType = 'success' | 'error' | 'warning' | 'info';
|
||||
|
||||
export interface ToastAction {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export interface Toast {
|
||||
id: string;
|
||||
type: ToastType;
|
||||
message: string;
|
||||
duration: number;
|
||||
action?: ToastAction;
|
||||
}
|
||||
|
||||
// State
|
||||
|
|
@ -50,11 +56,12 @@ export const toastStore = {
|
|||
* @param message - The message to display
|
||||
* @param type - Toast type: 'success' | 'error' | 'warning' | 'info'
|
||||
* @param duration - Duration in ms (0 = permanent, default: 4000)
|
||||
* @param action - Optional action button { label, onClick }
|
||||
* @returns The toast ID for manual dismissal
|
||||
*/
|
||||
show(message: string, type: ToastType = 'info', duration = 4000): string {
|
||||
show(message: string, type: ToastType = 'info', duration = 4000, action?: ToastAction): string {
|
||||
const id = generateId();
|
||||
const toast: Toast = { id, type, message, duration };
|
||||
const toast: Toast = { id, type, message, duration, action };
|
||||
|
||||
toasts = [...toasts, toast];
|
||||
|
||||
|
|
@ -104,6 +111,24 @@ export const toastStore = {
|
|||
return this.show(message, 'info', duration);
|
||||
},
|
||||
|
||||
/**
|
||||
* Show a success toast with an undo action button.
|
||||
* @param message - The message to display
|
||||
* @param onUndo - Callback when "Rückgängig" is clicked
|
||||
* @param duration - Duration in ms (default: 5000)
|
||||
*/
|
||||
undo(message: string, onUndo: () => void, duration = 5000): string {
|
||||
return this.show(message, 'success', duration, {
|
||||
label: 'Rückgängig',
|
||||
onClick: () => {
|
||||
onUndo();
|
||||
// Find and dismiss this toast after undo
|
||||
const id = toasts.find((t) => t.action?.onClick === onUndo)?.id;
|
||||
if (id) this.dismiss(id);
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Dismiss a specific toast by ID
|
||||
* @param id - The toast ID to dismiss
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue