cleanup delete buttons

This commit is contained in:
Andrew Davis 2025-04-04 00:41:57 +10:00
parent 5fcc1f4079
commit 7d5cf918fc
No known key found for this signature in database
GPG Key ID: 30AB5B89A109D044
4 changed files with 131 additions and 132 deletions

View File

@ -9,7 +9,6 @@ import Fieldset from "@/components/Fieldset";
import { MacroStepCard } from "@/components/MacroStepCard"; import { MacroStepCard } from "@/components/MacroStepCard";
import { DEFAULT_DELAY, MAX_STEPS_PER_MACRO, MAX_KEYS_PER_STEP } from "@/constants/macros"; import { DEFAULT_DELAY, MAX_STEPS_PER_MACRO, MAX_KEYS_PER_STEP } from "@/constants/macros";
import FieldLabel from "@/components/FieldLabel"; import FieldLabel from "@/components/FieldLabel";
import { ConfirmDialog } from "@/components/ConfirmDialog";
interface ValidationErrors { interface ValidationErrors {
name?: string; name?: string;
@ -26,11 +25,6 @@ interface MacroFormProps {
onCancel: () => void; onCancel: () => void;
isSubmitting?: boolean; isSubmitting?: boolean;
submitText?: string; submitText?: string;
showCancelConfirm?: boolean;
onCancelConfirm?: () => void;
showDelete?: boolean;
onDelete?: () => void;
isDeleting?: boolean;
} }
export function MacroForm({ export function MacroForm({
@ -39,17 +33,11 @@ export function MacroForm({
onCancel, onCancel,
isSubmitting = false, isSubmitting = false,
submitText = "Save Macro", submitText = "Save Macro",
showCancelConfirm = false,
onCancelConfirm,
showDelete = false,
onDelete,
isDeleting = false
}: MacroFormProps) { }: MacroFormProps) {
const [macro, setMacro] = useState<Partial<KeySequence>>(initialData); const [macro, setMacro] = useState<Partial<KeySequence>>(initialData);
const [keyQueries, setKeyQueries] = useState<Record<number, string>>({}); const [keyQueries, setKeyQueries] = useState<Record<number, string>>({});
const [errors, setErrors] = useState<ValidationErrors>({}); const [errors, setErrors] = useState<ValidationErrors>({});
const [errorMessage, setErrorMessage] = useState<string | null>(null); const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const showTemporaryError = (message: string) => { const showTemporaryError = (message: string) => {
setErrorMessage(message); setErrorMessage(message);
@ -261,70 +249,23 @@ export function MacroForm({
</div> </div>
)} )}
<div className="mt-6 flex items-center justify-between"> <div className="mt-6 flex items-center gap-x-2">
{showCancelConfirm ? ( <Button
<div className="flex items-center gap-2"> size="SM"
<span className="text-sm text-slate-600 dark:text-slate-400"> theme="primary"
Cancel changes? text={isSubmitting ? "Saving..." : submitText}
</span> onClick={handleSubmit}
<Button disabled={isSubmitting}
size="SM" />
theme="danger" <Button
text="Yes" size="SM"
onClick={onCancelConfirm} theme="light"
/> text="Cancel"
<Button onClick={onCancel}
size="SM" />
theme="light"
text="No"
onClick={() => onCancel()}
/>
</div>
) : (
<>
<div className="flex gap-x-2">
<Button
size="SM"
theme="primary"
text={isSubmitting ? "Saving..." : submitText}
onClick={handleSubmit}
disabled={isSubmitting}
/>
<Button
size="SM"
theme="light"
text="Cancel"
onClick={onCancel}
/>
</div>
{showDelete && (
<Button
size="SM"
theme="danger"
text={isDeleting ? "Deleting..." : "Delete Macro"}
onClick={() => setShowDeleteConfirm(true)}
disabled={isDeleting}
/>
)}
</>
)}
</div> </div>
</div> </div>
</div> </div>
<ConfirmDialog
open={showDeleteConfirm}
onClose={() => setShowDeleteConfirm(false)}
title="Delete Macro"
description="Are you sure you want to delete this macro? This action cannot be undone."
variant="danger"
confirmText={isDeleting ? "Deleting" : "Delete"}
onConfirm={() => {
onDelete?.();
setShowDeleteConfirm(false);
}}
isConfirming={isDeleting}
/>
</> </>
); );
} }

View File

@ -1,4 +1,4 @@
import { LuArrowUp, LuArrowDown, LuX } from "react-icons/lu"; import { LuArrowUp, LuArrowDown, LuX, LuTrash } from "react-icons/lu";
import { Button } from "@/components/Button"; import { Button } from "@/components/Button";
import { Combobox } from "@/components/Combobox"; import { Combobox } from "@/components/Combobox";
@ -6,7 +6,7 @@ import { SelectMenuBasic } from "@/components/SelectMenuBasic";
import Card from "@/components/Card"; import Card from "@/components/Card";
import { keys, modifiers, keyDisplayMap } from "@/keyboardMappings"; import { keys, modifiers, keyDisplayMap } from "@/keyboardMappings";
import { MAX_KEYS_PER_STEP } from "@/constants/macros"; import { MAX_KEYS_PER_STEP } from "@/constants/macros";
import FieldLabel from "@/components/FieldLabel";1 import FieldLabel from "@/components/FieldLabel";
// Filter out modifier keys since they're handled in the modifiers section // Filter out modifier keys since they're handled in the modifiers section
const modifierKeyPrefixes = ['Alt', 'Control', 'Shift', 'Meta']; const modifierKeyPrefixes = ['Alt', 'Control', 'Shift', 'Meta'];
@ -121,6 +121,7 @@ export function MacroStepCard({
size="XS" size="XS"
theme="danger" theme="danger"
text="Delete" text="Delete"
LeadingIcon={LuTrash}
onClick={onDelete} onClick={onDelete}
/> />
)} )}
@ -138,28 +139,20 @@ export function MacroStepCard({
</span> </span>
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
{mods.map(option => ( {mods.map(option => (
<span <Button
key={option.value} key={option.value}
className={`flex items-center px-2 py-1 rounded border cursor-pointer text-xs font-medium transition-colors ${ size="XS"
ensureArray(step.modifiers).includes(option.value) theme={ensureArray(step.modifiers).includes(option.value) ? "primary" : "light"}
? 'bg-blue-100 border-blue-300 text-blue-700 dark:bg-blue-900/40 dark:border-blue-600 dark:text-blue-200' text={option.label.split(' ')[1] || option.label}
: 'bg-slate-100 border-slate-200 text-slate-600 hover:bg-slate-200 dark:bg-slate-800 dark:border-slate-700 dark:text-slate-300 dark:hover:bg-slate-700' onClick={() => {
}`} const modifiersArray = ensureArray(step.modifiers);
> const isSelected = modifiersArray.includes(option.value);
<input const newModifiers = isSelected
type="checkbox" ? modifiersArray.filter(m => m !== option.value)
className="sr-only" : [...modifiersArray, option.value];
checked={ensureArray(step.modifiers).includes(option.value)} onModifierChange(newModifiers);
onChange={e => { }}
const modifiersArray = ensureArray(step.modifiers); />
const newModifiers = e.target.checked
? [...modifiersArray, option.value]
: modifiersArray.filter(m => m !== option.value);
onModifierChange(newModifiers);
}}
/>
{option.label.split(' ')[1] || option.label}
</span>
))} ))}
</div> </div>
</div> </div>
@ -169,7 +162,7 @@ export function MacroStepCard({
<div className="w-full flex flex-col gap-1"> <div className="w-full flex flex-col gap-1">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<FieldLabel label="Keys" info={`You can add up to a maximum of ${MAX_KEYS_PER_STEP} keys to press per step.`} /> <FieldLabel label="Keys" description={`Maximum ${MAX_KEYS_PER_STEP} keys per step.`} />
</div> </div>
<div className="flex flex-wrap gap-1 pb-2"> <div className="flex flex-wrap gap-1 pb-2">
{ensureArray(step.keys).map((key, keyIndex) => ( {ensureArray(step.keys).map((key, keyIndex) => (

View File

@ -1,10 +1,13 @@
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { LuTrash } from "react-icons/lu";
import { KeySequence, useMacrosStore } from "@/hooks/stores"; import { KeySequence, useMacrosStore } from "@/hooks/stores";
import { SettingsPageHeader } from "@/components/SettingsPageheader"; import { SettingsPageHeader } from "@/components/SettingsPageheader";
import { MacroForm } from "@/components/MacroForm"; import { MacroForm } from "@/components/MacroForm";
import notifications from "@/notifications"; import notifications from "@/notifications";
import { Button } from "@/components/Button";
import { ConfirmDialog } from "@/components/ConfirmDialog";
const normalizeSortOrders = (macros: KeySequence[]): KeySequence[] => { const normalizeSortOrders = (macros: KeySequence[]): KeySequence[] => {
return macros.map((macro, index) => ({ return macros.map((macro, index) => ({
@ -20,6 +23,7 @@ export default function SettingsMacrosEditRoute() {
const navigate = useNavigate(); const navigate = useNavigate();
const { macroId } = useParams<{ macroId: string }>(); const { macroId } = useParams<{ macroId: string }>();
const [macro, setMacro] = useState<KeySequence | null>(null); const [macro, setMacro] = useState<KeySequence | null>(null);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
useEffect(() => { useEffect(() => {
const foundMacro = macros.find(m => m.id === macroId); const foundMacro = macros.find(m => m.id === macroId);
@ -89,19 +93,40 @@ export default function SettingsMacrosEditRoute() {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<SettingsPageHeader <div className="flex items-center justify-between">
title="Edit Macro" <SettingsPageHeader
description="Modify your keyboard macro" title="Edit Macro"
/> description="Modify your keyboard macro"
/>
<Button
size="SM"
theme="danger"
text="Delete Macro"
LeadingIcon={LuTrash}
onClick={() => setShowDeleteConfirm(true)}
disabled={isDeleting}
/>
</div>
<MacroForm <MacroForm
initialData={macro} initialData={macro}
onSubmit={handleUpdateMacro} onSubmit={handleUpdateMacro}
onCancel={() => navigate("../")} onCancel={() => navigate("../")}
isSubmitting={isUpdating} isSubmitting={isUpdating}
submitText="Save Changes" submitText="Save Changes"
showDelete />
onDelete={handleDeleteMacro}
isDeleting={isDeleting} <ConfirmDialog
open={showDeleteConfirm}
onClose={() => setShowDeleteConfirm(false)}
title="Delete Macro"
description="Are you sure you want to delete this macro? This action cannot be undone."
variant="danger"
confirmText={isDeleting ? "Deleting" : "Delete"}
onConfirm={() => {
handleDeleteMacro();
setShowDeleteConfirm(false);
}}
isConfirming={isDeleting}
/> />
</div> </div>
); );

View File

@ -1,6 +1,6 @@
import { useEffect, Fragment, useMemo, useState, useCallback } from "react"; import { useEffect, Fragment, useMemo, useState, useCallback } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { LuPenLine, LuLoader, LuCopy, LuMoveRight, LuCornerDownRight, LuArrowUp, LuArrowDown } from "react-icons/lu"; import { LuPenLine, LuLoader, LuCopy, LuMoveRight, LuCornerDownRight, LuArrowUp, LuArrowDown, LuTrash } from "react-icons/lu";
import { KeySequence, useMacrosStore, generateMacroId } from "@/hooks/stores"; import { KeySequence, useMacrosStore, generateMacroId } from "@/hooks/stores";
import { SettingsPageHeader } from "@/components/SettingsPageheader"; import { SettingsPageHeader } from "@/components/SettingsPageheader";
@ -10,7 +10,7 @@ import Card from "@/components/Card";
import { MAX_TOTAL_MACROS, COPY_SUFFIX } from "@/constants/macros"; import { MAX_TOTAL_MACROS, COPY_SUFFIX } from "@/constants/macros";
import { keyDisplayMap, modifierDisplayMap } from "@/keyboardMappings"; import { keyDisplayMap, modifierDisplayMap } from "@/keyboardMappings";
import notifications from "@/notifications"; import notifications from "@/notifications";
import { SettingsItem } from "@/routes/devices.$id.settings"; import { ConfirmDialog } from "@/components/ConfirmDialog";
const normalizeSortOrders = (macros: KeySequence[]): KeySequence[] => { const normalizeSortOrders = (macros: KeySequence[]): KeySequence[] => {
return macros.map((macro, index) => ({ return macros.map((macro, index) => ({
@ -23,6 +23,8 @@ export default function SettingsMacrosRoute() {
const { macros, loading, initialized, loadMacros, saveMacros } = useMacrosStore(); const { macros, loading, initialized, loadMacros, saveMacros } = useMacrosStore();
const navigate = useNavigate(); const navigate = useNavigate();
const [actionLoadingId, setActionLoadingId] = useState<string | null>(null); const [actionLoadingId, setActionLoadingId] = useState<string | null>(null);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [macroToDelete, setMacroToDelete] = useState<KeySequence | null>(null);
const isMaxMacrosReached = useMemo(() => const isMaxMacrosReached = useMemo(() =>
macros.length >= MAX_TOTAL_MACROS, macros.length >= MAX_TOTAL_MACROS,
@ -98,6 +100,27 @@ export default function SettingsMacrosRoute() {
} }
}, [macros, saveMacros, setActionLoadingId]); }, [macros, saveMacros, setActionLoadingId]);
const handleDeleteMacro = useCallback(async () => {
if (!macroToDelete?.id) return;
setActionLoadingId(macroToDelete.id);
try {
const updatedMacros = normalizeSortOrders(macros.filter(m => m.id !== macroToDelete.id));
await saveMacros(updatedMacros);
notifications.success(`Macro "${macroToDelete.name}" deleted successfully`);
setShowDeleteConfirm(false);
setMacroToDelete(null);
} catch (error: unknown) {
if (error instanceof Error) {
notifications.error(`Failed to delete macro: ${error.message}`);
} else {
notifications.error("Failed to delete macro");
}
} finally {
setActionLoadingId(null);
}
}, [macroToDelete, macros, saveMacros]);
const MacroList = useMemo(() => ( const MacroList = useMemo(() => (
<div className="space-y-2"> <div className="space-y-2">
{macros.map((macro, index) => ( {macros.map((macro, index) => (
@ -176,11 +199,21 @@ export default function SettingsMacrosRoute() {
</div> </div>
<div className="flex items-center gap-1 ml-4"> <div className="flex items-center gap-1 ml-4">
<Button
size="XS"
theme="danger"
LeadingIcon={LuTrash}
onClick={() => {
setMacroToDelete(macro);
setShowDeleteConfirm(true);
}}
disabled={actionLoadingId === macro.id}
aria-label={`Delete macro ${macro.name}`}
/>
<Button <Button
size="XS" size="XS"
theme="light" theme="light"
LeadingIcon={LuCopy} LeadingIcon={LuCopy}
text="Duplicate"
onClick={() => handleDuplicateMacro(macro)} onClick={() => handleDuplicateMacro(macro)}
disabled={actionLoadingId === macro.id} disabled={actionLoadingId === macro.id}
aria-label={`Duplicate macro ${macro.name}`} aria-label={`Duplicate macro ${macro.name}`}
@ -198,37 +231,44 @@ export default function SettingsMacrosRoute() {
</div> </div>
</Card> </Card>
))} ))}
<ConfirmDialog
open={showDeleteConfirm}
onClose={() => {
setShowDeleteConfirm(false);
setMacroToDelete(null);
}}
title="Delete Macro"
description={`Are you sure you want to delete "${macroToDelete?.name}"? This action cannot be undone.`}
variant="danger"
confirmText={actionLoadingId === macroToDelete?.id ? "Deleting..." : "Delete"}
onConfirm={handleDeleteMacro}
isConfirming={actionLoadingId === macroToDelete?.id}
/>
</div> </div>
), [macros, actionLoadingId]); ), [macros, actionLoadingId, showDeleteConfirm, macroToDelete, handleDeleteMacro]);
return ( return (
<div className="space-y-4"> <div className="space-y-4">
{macros.length > 0 && ( {macros.length > 0 && (
<> <div className="flex items-center justify-between">
<SettingsPageHeader <SettingsPageHeader
title="Keyboard Macros" title="Keyboard Macros"
description="Create and manage keyboard macros for quick actions" description={`Create and manage keyboard macros for quick actions. Currently ${macros.length}/${MAX_TOTAL_MACROS} macros are active.`}
/> />
<div className="flex items-center justify-between mb-4"> { macros.length > 0 && (
<SettingsItem <div className="flex items-center pl-2">
title="Macros" <Button
description={`${loading ? '?' : macros.length}/${MAX_TOTAL_MACROS}`} size="SM"
> theme="primary"
{ macros.length > 0 && ( text={isMaxMacrosReached ? `Max Reached` : "Add New Macro"}
<div className="flex items-center gap-2"> onClick={() => navigate("add")}
<Button disabled={isMaxMacrosReached}
size="SM" aria-label="Add new macro"
theme="primary" />
text={isMaxMacrosReached ? `Max Reached` : "Add New Macro"} </div>
onClick={() => navigate("add")} )}
disabled={isMaxMacrosReached} </div>
aria-label="Add new macro"
/>
</div>
)}
</SettingsItem>
</div>
</>
)} )}
<div className="space-y-4"> <div className="space-y-4">