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 { 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}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -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) => (
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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">
|
||||||
|
|
Loading…
Reference in New Issue