mirror of https://github.com/jetkvm/kvm.git
cleanup delete buttons
This commit is contained in:
parent
5fcc1f4079
commit
7d5cf918fc
|
@ -9,7 +9,6 @@ import Fieldset from "@/components/Fieldset";
|
|||
import { MacroStepCard } from "@/components/MacroStepCard";
|
||||
import { DEFAULT_DELAY, MAX_STEPS_PER_MACRO, MAX_KEYS_PER_STEP } from "@/constants/macros";
|
||||
import FieldLabel from "@/components/FieldLabel";
|
||||
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
||||
|
||||
interface ValidationErrors {
|
||||
name?: string;
|
||||
|
@ -26,11 +25,6 @@ interface MacroFormProps {
|
|||
onCancel: () => void;
|
||||
isSubmitting?: boolean;
|
||||
submitText?: string;
|
||||
showCancelConfirm?: boolean;
|
||||
onCancelConfirm?: () => void;
|
||||
showDelete?: boolean;
|
||||
onDelete?: () => void;
|
||||
isDeleting?: boolean;
|
||||
}
|
||||
|
||||
export function MacroForm({
|
||||
|
@ -39,17 +33,11 @@ export function MacroForm({
|
|||
onCancel,
|
||||
isSubmitting = false,
|
||||
submitText = "Save Macro",
|
||||
showCancelConfirm = false,
|
||||
onCancelConfirm,
|
||||
showDelete = false,
|
||||
onDelete,
|
||||
isDeleting = false
|
||||
}: MacroFormProps) {
|
||||
const [macro, setMacro] = useState<Partial<KeySequence>>(initialData);
|
||||
const [keyQueries, setKeyQueries] = useState<Record<number, string>>({});
|
||||
const [errors, setErrors] = useState<ValidationErrors>({});
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
|
||||
const showTemporaryError = (message: string) => {
|
||||
setErrorMessage(message);
|
||||
|
@ -261,70 +249,23 @@ export function MacroForm({
|
|||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6 flex items-center justify-between">
|
||||
{showCancelConfirm ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Cancel changes?
|
||||
</span>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="danger"
|
||||
text="Yes"
|
||||
onClick={onCancelConfirm}
|
||||
/>
|
||||
<Button
|
||||
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 className="mt-6 flex items-center 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>
|
||||
</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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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 { Combobox } from "@/components/Combobox";
|
||||
|
@ -6,7 +6,7 @@ import { SelectMenuBasic } from "@/components/SelectMenuBasic";
|
|||
import Card from "@/components/Card";
|
||||
import { keys, modifiers, keyDisplayMap } from "@/keyboardMappings";
|
||||
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
|
||||
const modifierKeyPrefixes = ['Alt', 'Control', 'Shift', 'Meta'];
|
||||
|
@ -121,6 +121,7 @@ export function MacroStepCard({
|
|||
size="XS"
|
||||
theme="danger"
|
||||
text="Delete"
|
||||
LeadingIcon={LuTrash}
|
||||
onClick={onDelete}
|
||||
/>
|
||||
)}
|
||||
|
@ -138,28 +139,20 @@ export function MacroStepCard({
|
|||
</span>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{mods.map(option => (
|
||||
<span
|
||||
<Button
|
||||
key={option.value}
|
||||
className={`flex items-center px-2 py-1 rounded border cursor-pointer text-xs font-medium transition-colors ${
|
||||
ensureArray(step.modifiers).includes(option.value)
|
||||
? 'bg-blue-100 border-blue-300 text-blue-700 dark:bg-blue-900/40 dark:border-blue-600 dark:text-blue-200'
|
||||
: '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'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="sr-only"
|
||||
checked={ensureArray(step.modifiers).includes(option.value)}
|
||||
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>
|
||||
size="XS"
|
||||
theme={ensureArray(step.modifiers).includes(option.value) ? "primary" : "light"}
|
||||
text={option.label.split(' ')[1] || option.label}
|
||||
onClick={() => {
|
||||
const modifiersArray = ensureArray(step.modifiers);
|
||||
const isSelected = modifiersArray.includes(option.value);
|
||||
const newModifiers = isSelected
|
||||
? modifiersArray.filter(m => m !== option.value)
|
||||
: [...modifiersArray, option.value];
|
||||
onModifierChange(newModifiers);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -169,7 +162,7 @@ export function MacroStepCard({
|
|||
|
||||
<div className="w-full flex flex-col 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 className="flex flex-wrap gap-1 pb-2">
|
||||
{ensureArray(step.keys).map((key, keyIndex) => (
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useState, useEffect } from "react";
|
||||
import { LuTrash } from "react-icons/lu";
|
||||
|
||||
import { KeySequence, useMacrosStore } from "@/hooks/stores";
|
||||
import { SettingsPageHeader } from "@/components/SettingsPageheader";
|
||||
import { MacroForm } from "@/components/MacroForm";
|
||||
import notifications from "@/notifications";
|
||||
import { Button } from "@/components/Button";
|
||||
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
||||
|
||||
const normalizeSortOrders = (macros: KeySequence[]): KeySequence[] => {
|
||||
return macros.map((macro, index) => ({
|
||||
|
@ -20,6 +23,7 @@ export default function SettingsMacrosEditRoute() {
|
|||
const navigate = useNavigate();
|
||||
const { macroId } = useParams<{ macroId: string }>();
|
||||
const [macro, setMacro] = useState<KeySequence | null>(null);
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const foundMacro = macros.find(m => m.id === macroId);
|
||||
|
@ -89,19 +93,40 @@ export default function SettingsMacrosEditRoute() {
|
|||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
title="Edit Macro"
|
||||
description="Modify your keyboard macro"
|
||||
/>
|
||||
<div className="flex items-center justify-between">
|
||||
<SettingsPageHeader
|
||||
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
|
||||
initialData={macro}
|
||||
onSubmit={handleUpdateMacro}
|
||||
onCancel={() => navigate("../")}
|
||||
isSubmitting={isUpdating}
|
||||
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>
|
||||
);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useEffect, Fragment, useMemo, useState, useCallback } from "react";
|
||||
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 { SettingsPageHeader } from "@/components/SettingsPageheader";
|
||||
|
@ -10,7 +10,7 @@ import Card from "@/components/Card";
|
|||
import { MAX_TOTAL_MACROS, COPY_SUFFIX } from "@/constants/macros";
|
||||
import { keyDisplayMap, modifierDisplayMap } from "@/keyboardMappings";
|
||||
import notifications from "@/notifications";
|
||||
import { SettingsItem } from "@/routes/devices.$id.settings";
|
||||
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
||||
|
||||
const normalizeSortOrders = (macros: KeySequence[]): KeySequence[] => {
|
||||
return macros.map((macro, index) => ({
|
||||
|
@ -23,6 +23,8 @@ export default function SettingsMacrosRoute() {
|
|||
const { macros, loading, initialized, loadMacros, saveMacros } = useMacrosStore();
|
||||
const navigate = useNavigate();
|
||||
const [actionLoadingId, setActionLoadingId] = useState<string | null>(null);
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
const [macroToDelete, setMacroToDelete] = useState<KeySequence | null>(null);
|
||||
|
||||
const isMaxMacrosReached = useMemo(() =>
|
||||
macros.length >= MAX_TOTAL_MACROS,
|
||||
|
@ -98,6 +100,27 @@ export default function SettingsMacrosRoute() {
|
|||
}
|
||||
}, [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(() => (
|
||||
<div className="space-y-2">
|
||||
{macros.map((macro, index) => (
|
||||
|
@ -176,11 +199,21 @@ export default function SettingsMacrosRoute() {
|
|||
</div>
|
||||
|
||||
<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
|
||||
size="XS"
|
||||
theme="light"
|
||||
LeadingIcon={LuCopy}
|
||||
text="Duplicate"
|
||||
onClick={() => handleDuplicateMacro(macro)}
|
||||
disabled={actionLoadingId === macro.id}
|
||||
aria-label={`Duplicate macro ${macro.name}`}
|
||||
|
@ -198,37 +231,44 @@ export default function SettingsMacrosRoute() {
|
|||
</div>
|
||||
</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>
|
||||
), [macros, actionLoadingId]);
|
||||
), [macros, actionLoadingId, showDeleteConfirm, macroToDelete, handleDeleteMacro]);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{macros.length > 0 && (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<SettingsPageHeader
|
||||
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">
|
||||
<SettingsItem
|
||||
title="Macros"
|
||||
description={`${loading ? '?' : macros.length}/${MAX_TOTAL_MACROS}`}
|
||||
>
|
||||
{ macros.length > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text={isMaxMacrosReached ? `Max Reached` : "Add New Macro"}
|
||||
onClick={() => navigate("add")}
|
||||
disabled={isMaxMacrosReached}
|
||||
aria-label="Add new macro"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</SettingsItem>
|
||||
</div>
|
||||
</>
|
||||
{ macros.length > 0 && (
|
||||
<div className="flex items-center pl-2">
|
||||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text={isMaxMacrosReached ? `Max Reached` : "Add New Macro"}
|
||||
onClick={() => navigate("add")}
|
||||
disabled={isMaxMacrosReached}
|
||||
aria-label="Add new macro"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
|
|
Loading…
Reference in New Issue