diff --git a/apps/mana/apps/web/src/lib/data/tag-mutations.ts b/apps/mana/apps/web/src/lib/data/tag-mutations.ts new file mode 100644 index 000000000..a710f8a01 --- /dev/null +++ b/apps/mana/apps/web/src/lib/data/tag-mutations.ts @@ -0,0 +1,67 @@ +/** + * Tag mutation helpers — shared add/remove logic for entity.tagIds fields. + * + * Every module that supports tagging on its records (calendar events, + * contacts, places, todo tasks, …) reimplemented the same two operations: + * + * - "append a tag if not already present" (drag-drop from tag strip) + * - "remove a tag, with undo toast" (click on a tag pill in the detail view) + * + * These helpers stay store-agnostic — the caller passes the current + * `tagIds` array and an `update` function that knows which store/field to + * write to. That keeps them usable for tasks (which write `metadata.labelIds` + * via `tasksStore.updateLabels`) as well as the standard `tagIds` modules. + */ + +import { toastStore } from '@mana/shared-ui/toast'; + +type UpdateFn = (next: string[]) => Promise | void; + +/** + * Append `tagId` to `current` and call `update` with the result. + * No-op if the tag is already present. + * + * @example + * ```svelte + * use:dropTarget={{ + * accepts: ['tag'], + * onDrop: (p) => addTagId( + * contact.tagIds ?? [], + * (p.data as TagDragData).id, + * (next) => contactsStore.updateTagIds(contact.id, next), + * ), + * }} + * ``` + */ +export async function addTagId(current: string[], tagId: string, update: UpdateFn): Promise { + if (current.includes(tagId)) return; + await update([...current, tagId]); +} + +/** + * Remove `tagId` from `current`, call `update` with the filtered list, + * and show a toast with an undo action that reinstates the original list. + * + * @example + * ```ts + * async function removeTag(tagId: string) { + * await removeTagIdWithUndo( + * contact.tagIds ?? [], + * tagId, + * (next) => contactsStore.updateTagIds(contactId, next), + * ); + * } + * ``` + */ +export async function removeTagIdWithUndo( + current: string[], + tagId: string, + update: UpdateFn, + undoLabel = 'Tag entfernt' +): Promise { + const removed = current.filter((id) => id !== tagId); + await update(removed); + toastStore.undo(undoLabel, () => { + void update(current); + }); +} diff --git a/apps/mana/apps/web/src/lib/modules/calendar/ListView.svelte b/apps/mana/apps/web/src/lib/modules/calendar/ListView.svelte index 2fd04a77f..1a82a300b 100644 --- a/apps/mana/apps/web/src/lib/modules/calendar/ListView.svelte +++ b/apps/mana/apps/web/src/lib/modules/calendar/ListView.svelte @@ -14,6 +14,7 @@ import { dropTarget, dragSource } from '@mana/shared-ui/dnd'; import type { TagDragData } from '@mana/shared-ui/dnd'; import { useAllTags, getTagsByIds } from '@mana/shared-stores'; + import { addTagId } from '$lib/data/tag-mutations'; let { navigate, goBack, params }: ViewProps = $props(); @@ -23,10 +24,9 @@ function handleTagDrop(eventId: string, tagData: TagDragData) { const event = allItems.find((e) => e.id === eventId); if (!event) return; - const current = event.tagIds ?? []; - if (!current.includes(tagData.id)) { - eventsStore.updateTagIds(eventId, [...current, tagData.id]); - } + void addTagId(event.tagIds ?? [], tagData.id, (next) => + eventsStore.updateTagIds(eventId, next) + ); } const itemsQuery = useAllCalendarItems(); diff --git a/apps/mana/apps/web/src/lib/modules/calendar/views/DetailView.svelte b/apps/mana/apps/web/src/lib/modules/calendar/views/DetailView.svelte index c625beacc..f19b95aa5 100644 --- a/apps/mana/apps/web/src/lib/modules/calendar/views/DetailView.svelte +++ b/apps/mana/apps/web/src/lib/modules/calendar/views/DetailView.svelte @@ -15,6 +15,7 @@ import { useAllTags, getTagsByIds } from '@mana/shared-stores'; import LinkedItems from '$lib/components/links/LinkedItems.svelte'; import { toastStore } from '@mana/shared-ui/toast'; + import { removeTagIdWithUndo } from '$lib/data/tag-mutations'; let { navigate, params, goBack }: ViewProps = $props(); let eventId = $derived(params.eventId as string); @@ -68,12 +69,9 @@ let eventTags = $derived(getTagsByIds(allTags, detail.entity?.tagIds ?? [])); async function removeTag(tagId: string) { - const current = detail.entity?.tagIds ?? []; - const removed = current.filter((id) => id !== tagId); - await eventsStore.updateTagIds(eventId, removed); - toastStore.undo('Tag entfernt', () => { - eventsStore.updateTagIds(eventId, current); - }); + await removeTagIdWithUndo(detail.entity?.tagIds ?? [], tagId, (next) => + eventsStore.updateTagIds(eventId, next) + ); } async function saveField() { diff --git a/apps/mana/apps/web/src/lib/modules/contacts/ListView.svelte b/apps/mana/apps/web/src/lib/modules/contacts/ListView.svelte index 6edf5c052..ce19e7bf3 100644 --- a/apps/mana/apps/web/src/lib/modules/contacts/ListView.svelte +++ b/apps/mana/apps/web/src/lib/modules/contacts/ListView.svelte @@ -14,6 +14,7 @@ import { dropTarget, dragSource } from '@mana/shared-ui/dnd'; import type { TagDragData } from '@mana/shared-ui/dnd'; import { useAllTags, getTagsByIds } from '@mana/shared-stores'; + import { addTagId } from '$lib/data/tag-mutations'; let { navigate, goBack, params }: ViewProps = $props(); @@ -23,10 +24,9 @@ function handleTagDrop(contactId: string, tagData: TagDragData) { const contact = contacts.find((c) => c.id === contactId); if (!contact) return; - const current = contact.tagIds ?? []; - if (!current.includes(tagData.id)) { - contactsStore.updateTagIds(contactId, [...current, tagData.id]); - } + void addTagId(contact.tagIds ?? [], tagData.id, (next) => + contactsStore.updateTagIds(contactId, next) + ); } let contacts$ = useLiveQueryWithDefault(async () => { diff --git a/apps/mana/apps/web/src/lib/modules/contacts/views/DetailView.svelte b/apps/mana/apps/web/src/lib/modules/contacts/views/DetailView.svelte index 86b9338c3..478d0fba5 100644 --- a/apps/mana/apps/web/src/lib/modules/contacts/views/DetailView.svelte +++ b/apps/mana/apps/web/src/lib/modules/contacts/views/DetailView.svelte @@ -11,7 +11,7 @@ import type { LocalContact } from '../types'; import { useAllTags, getTagsByIds } from '@mana/shared-stores'; import LinkedItems from '$lib/components/links/LinkedItems.svelte'; - import { toastStore } from '@mana/shared-ui/toast'; + import { removeTagIdWithUndo } from '$lib/data/tag-mutations'; let { navigate, params, goBack }: ViewProps = $props(); let contactId = $derived(params.contactId as string); @@ -58,12 +58,9 @@ let contactTags = $derived(getTagsByIds(allTags, detail.entity?.tagIds ?? [])); async function removeTag(tagId: string) { - const current = detail.entity?.tagIds ?? []; - const removed = current.filter((id) => id !== tagId); - await contactsStore.updateTagIds(contactId, removed); - toastStore.undo('Tag entfernt', () => { - contactsStore.updateTagIds(contactId, current); - }); + await removeTagIdWithUndo(detail.entity?.tagIds ?? [], tagId, (next) => + contactsStore.updateTagIds(contactId, next) + ); } function initials(c: LocalContact): string { diff --git a/apps/mana/apps/web/src/lib/modules/places/ListView.svelte b/apps/mana/apps/web/src/lib/modules/places/ListView.svelte index 5297d8b69..f74a9b5a9 100644 --- a/apps/mana/apps/web/src/lib/modules/places/ListView.svelte +++ b/apps/mana/apps/web/src/lib/modules/places/ListView.svelte @@ -14,6 +14,7 @@ import { dropTarget, dragSource } from '@mana/shared-ui/dnd'; import type { TagDragData } from '@mana/shared-ui/dnd'; import { useAllTags, getTagsByIds } from '@mana/shared-stores'; + import { addTagId } from '$lib/data/tag-mutations'; let { navigate, goBack, params }: ViewProps = $props(); @@ -23,10 +24,9 @@ function handleTagDrop(placeId: string, tagData: TagDragData) { const place = places.find((p) => p.id === placeId); if (!place) return; - const current = place.tagIds ?? []; - if (!current.includes(tagData.id)) { - placesStore.updateTagIds(placeId, [...current, tagData.id]); - } + void addTagId(place.tagIds ?? [], tagData.id, (next) => + placesStore.updateTagIds(placeId, next) + ); } let places = $state([]); diff --git a/apps/mana/apps/web/src/lib/modules/places/views/DetailView.svelte b/apps/mana/apps/web/src/lib/modules/places/views/DetailView.svelte index a808e68d7..9647e4b30 100644 --- a/apps/mana/apps/web/src/lib/modules/places/views/DetailView.svelte +++ b/apps/mana/apps/web/src/lib/modules/places/views/DetailView.svelte @@ -13,6 +13,7 @@ import type { LocalPlace, PlaceCategory, LocalLocationLog } from '../types'; import { useAllTags, getTagsByIds } from '@mana/shared-stores'; import LinkedItems from '$lib/components/links/LinkedItems.svelte'; + import { removeTagIdWithUndo } from '$lib/data/tag-mutations'; let { navigate, params, goBack }: ViewProps = $props(); let placeId = $derived(params.placeId as string); @@ -64,10 +65,8 @@ ]; async function removeTag(tagId: string) { - const current = detail.entity?.tagIds ?? []; - await placesStore.updateTagIds( - placeId, - current.filter((id) => id !== tagId) + await removeTagIdWithUndo(detail.entity?.tagIds ?? [], tagId, (next) => + placesStore.updateTagIds(placeId, next) ); } diff --git a/apps/mana/apps/web/src/lib/modules/todo/ListView.svelte b/apps/mana/apps/web/src/lib/modules/todo/ListView.svelte index a0a0c5e3f..e63945b4e 100644 --- a/apps/mana/apps/web/src/lib/modules/todo/ListView.svelte +++ b/apps/mana/apps/web/src/lib/modules/todo/ListView.svelte @@ -20,6 +20,7 @@ import { dropTarget, dragSource } from '@mana/shared-ui/dnd'; import type { TagDragData } from '@mana/shared-ui/dnd'; import { useAllTags, getTagsByIds } from '@mana/shared-stores'; + import { addTagId } from '$lib/data/tag-mutations'; import VoiceCaptureBar from '$lib/components/voice/VoiceCaptureBar.svelte'; let { navigate, goBack, params }: ViewProps = $props(); @@ -34,10 +35,7 @@ function handleTagDrop(taskId: string, tagData: TagDragData) { const task = tasks.find((t) => t.id === taskId); if (!task) return; - const current = getTaskTagIds(task); - if (!current.includes(tagData.id)) { - tasksStore.updateLabels(taskId, [...current, tagData.id]); - } + void addTagId(getTaskTagIds(task), tagData.id, (next) => tasksStore.updateLabels(taskId, next)); } type ViewFilter = 'inbox' | 'today' | 'overdue'; diff --git a/apps/mana/apps/web/src/lib/modules/todo/views/DetailView.svelte b/apps/mana/apps/web/src/lib/modules/todo/views/DetailView.svelte index 0bfdf6f02..44eb4fb0d 100644 --- a/apps/mana/apps/web/src/lib/modules/todo/views/DetailView.svelte +++ b/apps/mana/apps/web/src/lib/modules/todo/views/DetailView.svelte @@ -17,6 +17,7 @@ import { useAllTags, getTagsByIds } from '@mana/shared-stores'; import LinkedItems from '$lib/components/links/LinkedItems.svelte'; import { toastStore } from '@mana/shared-ui/toast'; + import { removeTagIdWithUndo } from '$lib/data/tag-mutations'; let { navigate, params, goBack }: ViewProps = $props(); let taskId = $derived(params.taskId as string); @@ -73,12 +74,9 @@ let taskTags = $derived(getTagsByIds(allTags, getTaskTagIds())); async function removeTag(tagId: string) { - const current = getTaskTagIds(); - const removed = current.filter((id) => id !== tagId); - await tasksStore.updateLabels(taskId, removed); - toastStore.undo('Tag entfernt', () => { - tasksStore.updateLabels(taskId, current); - }); + await removeTagIdWithUndo(getTaskTagIds(), tagId, (next) => + tasksStore.updateLabels(taskId, next) + ); } async function saveField() {