diff --git a/TaskTracker.Web/src/pages/Mappings.tsx b/TaskTracker.Web/src/pages/Mappings.tsx index 0810884..5020f01 100644 --- a/TaskTracker.Web/src/pages/Mappings.tsx +++ b/TaskTracker.Web/src/pages/Mappings.tsx @@ -1,7 +1,288 @@ +import { useState } from 'react' +import { Pencil, Trash2, Check, X, Plus } from 'lucide-react' +import { useMappings, useCreateMapping, useUpdateMapping, useDeleteMapping } from '../api/mappings' +import { CATEGORY_COLORS } from '../lib/constants' +import type { AppMapping } from '../types' + +const MATCH_TYPES = ['ProcessName', 'TitleContains', 'UrlContains'] as const + +const MATCH_TYPE_COLORS: Record = { + ProcessName: '#6366f1', + TitleContains: '#06b6d4', + UrlContains: '#f97316', +} + +interface FormData { + pattern: string + matchType: string + category: string + friendlyName: string +} + +const emptyForm: FormData = { pattern: '', matchType: 'ProcessName', category: '', friendlyName: '' } + +function formFromMapping(m: AppMapping): FormData { + return { + pattern: m.pattern, + matchType: m.matchType, + category: m.category, + friendlyName: m.friendlyName ?? '', + } +} + export default function Mappings() { + const { data: mappings, isLoading } = useMappings() + const createMapping = useCreateMapping() + const updateMapping = useUpdateMapping() + const deleteMapping = useDeleteMapping() + + const [addingNew, setAddingNew] = useState(false) + const [newForm, setNewForm] = useState(emptyForm) + const [editingId, setEditingId] = useState(null) + const [editForm, setEditForm] = useState(emptyForm) + + function handleAddSave() { + if (!newForm.pattern.trim() || !newForm.category.trim()) return + createMapping.mutate( + { + pattern: newForm.pattern.trim(), + matchType: newForm.matchType, + category: newForm.category.trim(), + friendlyName: newForm.friendlyName.trim() || undefined, + }, + { + onSuccess: () => { + setAddingNew(false) + setNewForm(emptyForm) + }, + }, + ) + } + + function handleAddCancel() { + setAddingNew(false) + setNewForm(emptyForm) + } + + function handleEditStart(mapping: AppMapping) { + setEditingId(mapping.id) + setEditForm(formFromMapping(mapping)) + // Cancel any add-new row when starting an edit + setAddingNew(false) + } + + function handleEditSave() { + if (editingId === null) return + if (!editForm.pattern.trim() || !editForm.category.trim()) return + updateMapping.mutate( + { + id: editingId, + pattern: editForm.pattern.trim(), + matchType: editForm.matchType, + category: editForm.category.trim(), + friendlyName: editForm.friendlyName.trim() || undefined, + }, + { + onSuccess: () => { + setEditingId(null) + setEditForm(emptyForm) + }, + }, + ) + } + + function handleEditCancel() { + setEditingId(null) + setEditForm(emptyForm) + } + + function handleDelete(id: number) { + if (!window.confirm('Delete this mapping rule?')) return + deleteMapping.mutate(id) + } + + const inputClass = + 'bg-[#0f1117] text-white text-sm rounded border border-white/10 px-2 py-1.5 focus:outline-none focus:border-indigo-500 transition-colors w-full' + const selectClass = + 'bg-[#0f1117] text-white text-sm rounded border border-white/10 px-2 py-1.5 focus:outline-none focus:border-indigo-500 transition-colors appearance-none cursor-pointer w-full' + + function renderFormRow(form: FormData, setForm: (f: FormData) => void, onSave: () => void, onCancel: () => void, isSaving: boolean) { + return ( + + + setForm({ ...form, pattern: e.target.value })} + className={inputClass} + autoFocus + /> + + + + + + setForm({ ...form, category: e.target.value })} + className={inputClass} + /> + + + setForm({ ...form, friendlyName: e.target.value })} + className={inputClass} + /> + + +
+ + +
+ + + ) + } + return ( -
-

Mappings

+
+ {/* Header */} +
+

App Mappings

+ +
+ + {/* Table */} + {isLoading ? ( +
Loading mappings...
+ ) : !mappings?.length && !addingNew ? ( +
+

No mappings configured

+ +
+ ) : ( +
+ + + + + + + + + + + + {/* Add-new row */} + {addingNew && + renderFormRow(newForm, setNewForm, handleAddSave, handleAddCancel, createMapping.isPending)} + + {/* Data rows */} + {mappings?.map((m) => + editingId === m.id ? ( + renderFormRow(editForm, setEditForm, handleEditSave, handleEditCancel, updateMapping.isPending) + ) : ( + + + + + + + + ), + )} + +
PatternMatch TypeCategoryFriendly NameActions
{m.pattern} + + {m.matchType} + + + + + {m.category} + + + {m.friendlyName ?? '\u2014'} + +
+ + +
+
+
+ )}
) }