mirror of https://github.com/jetkvm/kvm.git
Fix lint errors
This commit is contained in:
parent
df74cb111d
commit
7cd3d32926
|
@ -1,7 +1,14 @@
|
|||
import { useRef } from "react";
|
||||
import clsx from "clsx";
|
||||
import { Combobox as HeadlessCombobox, ComboboxInput, ComboboxOption, ComboboxOptions } from "@headlessui/react";
|
||||
import {
|
||||
Combobox as HeadlessCombobox,
|
||||
ComboboxInput,
|
||||
ComboboxOption,
|
||||
ComboboxOptions,
|
||||
} from "@headlessui/react";
|
||||
|
||||
import { cva } from "@/cva.config";
|
||||
|
||||
import Card from "./Card";
|
||||
|
||||
export interface ComboboxOption {
|
||||
|
@ -22,7 +29,7 @@ const comboboxVariants = cva({
|
|||
|
||||
type BaseProps = React.ComponentProps<typeof HeadlessCombobox>;
|
||||
|
||||
interface ComboboxProps extends Omit<BaseProps, 'displayValue'> {
|
||||
interface ComboboxProps extends Omit<BaseProps, "displayValue"> {
|
||||
displayValue: (option: ComboboxOption) => string;
|
||||
onInputChange: (option: string) => void;
|
||||
options: () => ComboboxOption[];
|
||||
|
@ -48,10 +55,7 @@ export function Combobox({
|
|||
const classes = comboboxVariants({ size });
|
||||
|
||||
return (
|
||||
<HeadlessCombobox
|
||||
onChange={onChange}
|
||||
{...otherProps}
|
||||
>
|
||||
<HeadlessCombobox onChange={onChange} {...otherProps}>
|
||||
{() => (
|
||||
<>
|
||||
<Card className="w-auto !border border-solid !border-slate-800/30 shadow outline-0 dark:!border-slate-300/30">
|
||||
|
@ -73,30 +77,31 @@ export function Combobox({
|
|||
"focus:outline-blue-600 focus:ring-2 focus:ring-blue-700 focus:ring-offset-2 dark:focus:outline-blue-500 dark:focus:ring-blue-500",
|
||||
|
||||
// Disabled
|
||||
disabled && "pointer-events-none select-none bg-slate-50 text-slate-500/80 dark:bg-slate-800 dark:text-slate-400/80 disabled:hover:bg-white dark:disabled:hover:bg-slate-800"
|
||||
disabled &&
|
||||
"pointer-events-none select-none bg-slate-50 text-slate-500/80 disabled:hover:bg-white dark:bg-slate-800 dark:text-slate-400/80 dark:disabled:hover:bg-slate-800",
|
||||
)}
|
||||
placeholder={disabled ? disabledMessage : placeholder}
|
||||
displayValue={displayValue}
|
||||
onChange={(event) => onInputChange(event.target.value)}
|
||||
onChange={event => onInputChange(event.target.value)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{options().length > 0 && (
|
||||
<ComboboxOptions className="absolute left-0 z-[100] mt-1 w-full max-h-60 overflow-auto rounded-md bg-white py-1 text-sm shadow-lg ring-1 ring-black/5 dark:bg-slate-800 dark:ring-slate-700 hide-scrollbar">
|
||||
{options().map((option) => (
|
||||
<ComboboxOptions className="hide-scrollbar absolute left-0 z-[100] mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-sm shadow-lg ring-1 ring-black/5 dark:bg-slate-800 dark:ring-slate-700">
|
||||
{options().map(option => (
|
||||
<ComboboxOption
|
||||
key={option.value}
|
||||
value={option}
|
||||
className={clsx(
|
||||
// General styling
|
||||
"cursor-default select-none py-2 px-4",
|
||||
"cursor-default select-none px-4 py-2",
|
||||
|
||||
// Hover and active states
|
||||
"hover:bg-blue-50/80 ui-active:bg-blue-50/80 ui-active:text-blue-900",
|
||||
|
||||
// Dark mode
|
||||
"dark:text-slate-300 dark:hover:bg-slate-700 dark:ui-active:bg-slate-700 dark:ui-active:text-blue-200"
|
||||
"dark:text-slate-300 dark:hover:bg-slate-700 dark:ui-active:bg-slate-700 dark:ui-active:text-blue-200",
|
||||
)}
|
||||
>
|
||||
{option.label}
|
||||
|
@ -106,10 +111,8 @@ export function Combobox({
|
|||
)}
|
||||
|
||||
{options().length === 0 && inputRef.current?.value && (
|
||||
<div className="absolute left-0 z-[100] mt-1 w-full rounded-md bg-white dark:bg-slate-800 py-2 px-4 text-sm shadow-lg ring-1 ring-black/5 dark:ring-slate-700">
|
||||
<div className="text-slate-500 dark:text-slate-400">
|
||||
{emptyMessage}
|
||||
</div>
|
||||
<div className="absolute left-0 z-[100] mt-1 w-full rounded-md bg-white px-4 py-2 text-sm shadow-lg ring-1 ring-black/5 dark:bg-slate-800 dark:ring-slate-700">
|
||||
<div className="text-slate-500 dark:text-slate-400">{emptyMessage}</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
import { ExclamationTriangleIcon, CheckCircleIcon, InformationCircleIcon } from "@heroicons/react/24/outline";
|
||||
import {
|
||||
ExclamationTriangleIcon,
|
||||
CheckCircleIcon,
|
||||
InformationCircleIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
|
||||
import { cx } from "@/cva.config";
|
||||
import { Button } from "@/components/Button";
|
||||
import Modal from "@/components/Modal";
|
||||
|
@ -42,12 +47,15 @@ const variantConfig = {
|
|||
iconBgClass: "bg-blue-100",
|
||||
buttonTheme: "primary",
|
||||
},
|
||||
} as Record<Variant, {
|
||||
} as Record<
|
||||
Variant,
|
||||
{
|
||||
icon: React.ElementType;
|
||||
iconClass: string;
|
||||
iconBgClass: string;
|
||||
buttonTheme: "danger" | "primary" | "blank" | "light" | "lightDanger";
|
||||
}>;
|
||||
}
|
||||
>;
|
||||
|
||||
export function ConfirmDialog({
|
||||
open,
|
||||
|
@ -65,13 +73,18 @@ export function ConfirmDialog({
|
|||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<div className="mx-auto max-w-xl px-4 transition-all duration-300 ease-in-out">
|
||||
<div className="relative w-full overflow-hidden rounded-lg bg-white p-6 text-left align-middle shadow-xl transition-all dark:bg-slate-800 pointer-events-auto">
|
||||
<div className="pointer-events-auto relative w-full overflow-hidden rounded-lg bg-white p-6 text-left align-middle shadow-xl transition-all dark:bg-slate-800">
|
||||
<div className="space-y-4">
|
||||
<div className="sm:flex sm:items-start">
|
||||
<div className={cx("mx-auto flex size-12 shrink-0 items-center justify-center rounded-full sm:mx-0 sm:size-10", iconBgClass)}>
|
||||
<div
|
||||
className={cx(
|
||||
"mx-auto flex size-12 shrink-0 items-center justify-center rounded-full sm:mx-0 sm:size-10",
|
||||
iconBgClass,
|
||||
)}
|
||||
>
|
||||
<Icon aria-hidden="true" className={cx("size-6", iconClass)} />
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
|
||||
<h2 className="text-lg font-bold leading-tight text-black dark:text-white">
|
||||
{title}
|
||||
</h2>
|
||||
|
@ -83,12 +96,7 @@ export function ConfirmDialog({
|
|||
|
||||
<div className="flex justify-end gap-x-2">
|
||||
{cancelText && (
|
||||
<Button
|
||||
size="SM"
|
||||
theme="blank"
|
||||
text={cancelText}
|
||||
onClick={onClose}
|
||||
/>
|
||||
<Button size="SM" theme="blank" text={cancelText} onClick={onClose} />
|
||||
)}
|
||||
<Button
|
||||
size="SM"
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { useState } from "react";
|
||||
|
||||
import { LuPlus } from "react-icons/lu";
|
||||
|
||||
import { KeySequence } from "@/hooks/stores";
|
||||
|
@ -7,16 +6,23 @@ import { Button } from "@/components/Button";
|
|||
import { InputFieldWithLabel, FieldError } from "@/components/InputField";
|
||||
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 {
|
||||
DEFAULT_DELAY,
|
||||
MAX_STEPS_PER_MACRO,
|
||||
MAX_KEYS_PER_STEP,
|
||||
} from "@/constants/macros";
|
||||
import FieldLabel from "@/components/FieldLabel";
|
||||
|
||||
interface ValidationErrors {
|
||||
name?: string;
|
||||
steps?: Record<number, {
|
||||
steps?: Record<
|
||||
number,
|
||||
{
|
||||
keys?: string;
|
||||
modifiers?: string;
|
||||
delay?: string;
|
||||
}>;
|
||||
}
|
||||
>;
|
||||
}
|
||||
|
||||
interface MacroFormProps {
|
||||
|
@ -57,12 +63,14 @@ export function MacroForm({
|
|||
if (!macro.steps?.length) {
|
||||
newErrors.steps = { 0: { keys: "At least one step is required" } };
|
||||
} else {
|
||||
const hasKeyOrModifier = macro.steps.some(step =>
|
||||
(step.keys?.length || 0) > 0 || (step.modifiers?.length || 0) > 0
|
||||
const hasKeyOrModifier = macro.steps.some(
|
||||
step => (step.keys?.length || 0) > 0 || (step.modifiers?.length || 0) > 0,
|
||||
);
|
||||
|
||||
if (!hasKeyOrModifier) {
|
||||
newErrors.steps = { 0: { keys: "At least one step must have keys or modifiers" } };
|
||||
newErrors.steps = {
|
||||
0: { keys: "At least one step must have keys or modifiers" },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -87,7 +95,10 @@ export function MacroForm({
|
|||
}
|
||||
};
|
||||
|
||||
const handleKeySelect = (stepIndex: number, option: { value: string | null; keys?: string[] }) => {
|
||||
const handleKeySelect = (
|
||||
stepIndex: number,
|
||||
option: { value: string | null; keys?: string[] },
|
||||
) => {
|
||||
const newSteps = [...(macro.steps || [])];
|
||||
if (!newSteps[stepIndex]) return;
|
||||
|
||||
|
@ -97,7 +108,9 @@ export function MacroForm({
|
|||
if (!newSteps[stepIndex].keys) {
|
||||
newSteps[stepIndex].keys = [];
|
||||
}
|
||||
const keysArray = Array.isArray(newSteps[stepIndex].keys) ? newSteps[stepIndex].keys : [];
|
||||
const keysArray = Array.isArray(newSteps[stepIndex].keys)
|
||||
? newSteps[stepIndex].keys
|
||||
: [];
|
||||
if (keysArray.length >= MAX_KEYS_PER_STEP) {
|
||||
showTemporaryError(`Maximum of ${MAX_KEYS_PER_STEP} keys per step allowed`);
|
||||
return;
|
||||
|
@ -148,9 +161,9 @@ export function MacroForm({
|
|||
setMacro({ ...macro, steps: newSteps });
|
||||
};
|
||||
|
||||
const handleStepMove = (stepIndex: number, direction: 'up' | 'down') => {
|
||||
const handleStepMove = (stepIndex: number, direction: "up" | "down") => {
|
||||
const newSteps = [...(macro.steps || [])];
|
||||
const newIndex = direction === 'up' ? stepIndex - 1 : stepIndex + 1;
|
||||
const newIndex = direction === "up" ? stepIndex - 1 : stepIndex + 1;
|
||||
[newSteps[stepIndex], newSteps[newIndex]] = [newSteps[newIndex], newSteps[stepIndex]];
|
||||
setMacro({ ...macro, steps: newSteps });
|
||||
};
|
||||
|
@ -181,7 +194,10 @@ export function MacroForm({
|
|||
<div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<div className="flex items-center gap-1">
|
||||
<FieldLabel label="Steps" description={`Keys/modifiers executed in sequence with a delay between each step.`} />
|
||||
<FieldLabel
|
||||
label="Steps"
|
||||
description={`Keys/modifiers executed in sequence with a delay between each step.`}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-slate-500 dark:text-slate-400">
|
||||
{macro.steps?.length || 0}/{MAX_STEPS_PER_MACRO} steps
|
||||
|
@ -199,18 +215,24 @@ export function MacroForm({
|
|||
key={stepIndex}
|
||||
step={step}
|
||||
stepIndex={stepIndex}
|
||||
onDelete={macro.steps && macro.steps.length > 1 ? () => {
|
||||
onDelete={
|
||||
macro.steps && macro.steps.length > 1
|
||||
? () => {
|
||||
const newSteps = [...(macro.steps || [])];
|
||||
newSteps.splice(stepIndex, 1);
|
||||
setMacro(prev => ({ ...prev, steps: newSteps }));
|
||||
} : undefined}
|
||||
onMoveUp={() => handleStepMove(stepIndex, 'up')}
|
||||
onMoveDown={() => handleStepMove(stepIndex, 'down')}
|
||||
onKeySelect={(option) => handleKeySelect(stepIndex, option)}
|
||||
onKeyQueryChange={(query) => handleKeyQueryChange(stepIndex, query)}
|
||||
keyQuery={keyQueries[stepIndex] || ''}
|
||||
onModifierChange={(modifiers) => handleModifierChange(stepIndex, modifiers)}
|
||||
onDelayChange={(delay) => handleDelayChange(stepIndex, delay)}
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onMoveUp={() => handleStepMove(stepIndex, "up")}
|
||||
onMoveDown={() => handleStepMove(stepIndex, "down")}
|
||||
onKeySelect={option => handleKeySelect(stepIndex, option)}
|
||||
onKeyQueryChange={query => handleKeyQueryChange(stepIndex, query)}
|
||||
keyQuery={keyQueries[stepIndex] || ""}
|
||||
onModifierChange={modifiers =>
|
||||
handleModifierChange(stepIndex, modifiers)
|
||||
}
|
||||
onDelayChange={delay => handleDelayChange(stepIndex, delay)}
|
||||
isLastStep={stepIndex === (macro.steps?.length || 0) - 1}
|
||||
/>
|
||||
))}
|
||||
|
@ -223,10 +245,12 @@ export function MacroForm({
|
|||
theme="light"
|
||||
fullWidth
|
||||
LeadingIcon={LuPlus}
|
||||
text={`Add Step ${isMaxStepsReached ? `(${MAX_STEPS_PER_MACRO} max)` : ''}`}
|
||||
text={`Add Step ${isMaxStepsReached ? `(${MAX_STEPS_PER_MACRO} max)` : ""}`}
|
||||
onClick={() => {
|
||||
if (isMaxStepsReached) {
|
||||
showTemporaryError(`You can only add a maximum of ${MAX_STEPS_PER_MACRO} steps per macro.`);
|
||||
showTemporaryError(
|
||||
`You can only add a maximum of ${MAX_STEPS_PER_MACRO} steps per macro.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -234,7 +258,7 @@ export function MacroForm({
|
|||
...prev,
|
||||
steps: [
|
||||
...(prev.steps || []),
|
||||
{ keys: [], modifiers: [], delay: DEFAULT_DELAY }
|
||||
{ keys: [], modifiers: [], delay: DEFAULT_DELAY },
|
||||
],
|
||||
}));
|
||||
setErrors({});
|
||||
|
@ -257,12 +281,7 @@ export function MacroForm({
|
|||
onClick={handleSubmit}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Cancel"
|
||||
onClick={onCancel}
|
||||
/>
|
||||
<Button size="SM" theme="light" text="Cancel" onClick={onCancel} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,11 +3,11 @@ import { ExclamationTriangleIcon } from "@heroicons/react/24/solid";
|
|||
import { ArrowPathIcon, ArrowRightIcon } from "@heroicons/react/16/solid";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { LuPlay } from "react-icons/lu";
|
||||
import { BsMouseFill } from "react-icons/bs";
|
||||
|
||||
import { Button, LinkButton } from "@components/Button";
|
||||
import LoadingSpinner from "@components/LoadingSpinner";
|
||||
import Card, { GridCard } from "@components/Card";
|
||||
import { BsMouseFill } from "react-icons/bs";
|
||||
|
||||
interface OverlayContentProps {
|
||||
children: React.ReactNode;
|
||||
|
@ -242,8 +242,8 @@ export function HDMIErrorOverlay({ show, hdmiState }: HDMIErrorOverlayProps) {
|
|||
Ensure source device is powered on and outputting a signal
|
||||
</li>
|
||||
<li>
|
||||
If using an adapter, ensure it's compatible and
|
||||
functioning correctly
|
||||
If using an adapter, ensure it's compatible and functioning
|
||||
correctly
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -151,7 +151,7 @@ export default function WebRTCVideo() {
|
|||
const isKeyboardLockGranted = await checkNavigatorPermissions("keyboard-lock");
|
||||
if (isKeyboardLockGranted) {
|
||||
if ("keyboard" in navigator) {
|
||||
// @ts-ignore
|
||||
// @ts-expect-error - keyboard lock is not supported in all browsers
|
||||
await navigator.keyboard.lock();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import { create } from "zustand";
|
||||
import { createJSONStorage, persist } from "zustand/middleware";
|
||||
import { MAX_STEPS_PER_MACRO, MAX_TOTAL_MACROS, MAX_KEYS_PER_STEP } from "@/constants/macros";
|
||||
|
||||
import {
|
||||
MAX_STEPS_PER_MACRO,
|
||||
MAX_TOTAL_MACROS,
|
||||
MAX_KEYS_PER_STEP,
|
||||
} from "@/constants/macros";
|
||||
|
||||
// Define the JsonRpc types for better type checking
|
||||
interface JsonRpcResponse {
|
||||
|
@ -719,12 +724,23 @@ export interface NetworkState {
|
|||
setDhcpLeaseExpiry: (expiry: Date) => void;
|
||||
}
|
||||
|
||||
|
||||
export type IPv6Mode = "disabled" | "slaac" | "dhcpv6" | "slaac_and_dhcpv6" | "static" | "link_local" | "unknown";
|
||||
export type IPv6Mode =
|
||||
| "disabled"
|
||||
| "slaac"
|
||||
| "dhcpv6"
|
||||
| "slaac_and_dhcpv6"
|
||||
| "static"
|
||||
| "link_local"
|
||||
| "unknown";
|
||||
export type IPv4Mode = "disabled" | "static" | "dhcp" | "unknown";
|
||||
export type LLDPMode = "disabled" | "basic" | "all" | "unknown";
|
||||
export type mDNSMode = "disabled" | "auto" | "ipv4_only" | "ipv6_only" | "unknown";
|
||||
export type TimeSyncMode = "ntp_only" | "ntp_and_http" | "http_only" | "custom" | "unknown";
|
||||
export type TimeSyncMode =
|
||||
| "ntp_only"
|
||||
| "ntp_and_http"
|
||||
| "http_only"
|
||||
| "custom"
|
||||
| "unknown";
|
||||
|
||||
export interface NetworkSettings {
|
||||
hostname: string;
|
||||
|
@ -749,7 +765,7 @@ export const useNetworkStateStore = create<NetworkState>((set, get) => ({
|
|||
|
||||
lease.lease_expiry = expiry;
|
||||
set({ dhcp_lease: lease });
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
export interface KeySequenceStep {
|
||||
|
@ -771,8 +787,20 @@ export interface MacrosState {
|
|||
initialized: boolean;
|
||||
loadMacros: () => Promise<void>;
|
||||
saveMacros: (macros: KeySequence[]) => Promise<void>;
|
||||
sendFn: ((method: string, params: unknown, callback?: ((resp: JsonRpcResponse) => void) | undefined) => void) | null;
|
||||
setSendFn: (sendFn: ((method: string, params: unknown, callback?: ((resp: JsonRpcResponse) => void) | undefined) => void)) => void;
|
||||
sendFn:
|
||||
| ((
|
||||
method: string,
|
||||
params: unknown,
|
||||
callback?: ((resp: JsonRpcResponse) => void) | undefined,
|
||||
) => void)
|
||||
| null;
|
||||
setSendFn: (
|
||||
sendFn: (
|
||||
method: string,
|
||||
params: unknown,
|
||||
callback?: ((resp: JsonRpcResponse) => void) | undefined,
|
||||
) => void,
|
||||
) => void;
|
||||
}
|
||||
|
||||
export const generateMacroId = () => {
|
||||
|
@ -785,7 +813,7 @@ export const useMacrosStore = create<MacrosState>((set, get) => ({
|
|||
initialized: false,
|
||||
sendFn: null,
|
||||
|
||||
setSendFn: (sendFn) => {
|
||||
setSendFn: sendFn => {
|
||||
set({ sendFn });
|
||||
},
|
||||
|
||||
|
@ -802,7 +830,7 @@ export const useMacrosStore = create<MacrosState>((set, get) => ({
|
|||
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
sendFn("getKeyboardMacros", {}, (response) => {
|
||||
sendFn("getKeyboardMacros", {}, response => {
|
||||
if (response.error) {
|
||||
console.error("Error loading macros:", response.error);
|
||||
reject(new Error(response.error.message));
|
||||
|
@ -822,7 +850,7 @@ export const useMacrosStore = create<MacrosState>((set, get) => ({
|
|||
|
||||
set({
|
||||
macros: sortedMacros,
|
||||
initialized: true
|
||||
initialized: true,
|
||||
});
|
||||
|
||||
resolve();
|
||||
|
@ -849,15 +877,23 @@ export const useMacrosStore = create<MacrosState>((set, get) => ({
|
|||
|
||||
for (const macro of macros) {
|
||||
if (macro.steps.length > MAX_STEPS_PER_MACRO) {
|
||||
console.error(`Cannot save: macro "${macro.name}" exceeds maximum of ${MAX_STEPS_PER_MACRO} steps`);
|
||||
throw new Error(`Cannot save: macro "${macro.name}" exceeds maximum of ${MAX_STEPS_PER_MACRO} steps`);
|
||||
console.error(
|
||||
`Cannot save: macro "${macro.name}" exceeds maximum of ${MAX_STEPS_PER_MACRO} steps`,
|
||||
);
|
||||
throw new Error(
|
||||
`Cannot save: macro "${macro.name}" exceeds maximum of ${MAX_STEPS_PER_MACRO} steps`,
|
||||
);
|
||||
}
|
||||
|
||||
for (let i = 0; i < macro.steps.length; i++) {
|
||||
const step = macro.steps[i];
|
||||
if (step.keys && step.keys.length > MAX_KEYS_PER_STEP) {
|
||||
console.error(`Cannot save: macro "${macro.name}" step ${i + 1} exceeds maximum of ${MAX_KEYS_PER_STEP} keys`);
|
||||
throw new Error(`Cannot save: macro "${macro.name}" step ${i + 1} exceeds maximum of ${MAX_KEYS_PER_STEP} keys`);
|
||||
console.error(
|
||||
`Cannot save: macro "${macro.name}" step ${i + 1} exceeds maximum of ${MAX_KEYS_PER_STEP} keys`,
|
||||
);
|
||||
throw new Error(
|
||||
`Cannot save: macro "${macro.name}" step ${i + 1} exceeds maximum of ${MAX_KEYS_PER_STEP} keys`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -867,18 +903,23 @@ export const useMacrosStore = create<MacrosState>((set, get) => ({
|
|||
try {
|
||||
const macrosWithSortOrder = macros.map((macro, index) => ({
|
||||
...macro,
|
||||
sortOrder: macro.sortOrder !== undefined ? macro.sortOrder : index
|
||||
sortOrder: macro.sortOrder !== undefined ? macro.sortOrder : index,
|
||||
}));
|
||||
|
||||
const response = await new Promise<JsonRpcResponse>((resolve) => {
|
||||
sendFn("setKeyboardMacros", { params: { macros: macrosWithSortOrder } }, (response) => {
|
||||
const response = await new Promise<JsonRpcResponse>(resolve => {
|
||||
sendFn(
|
||||
"setKeyboardMacros",
|
||||
{ params: { macros: macrosWithSortOrder } },
|
||||
response => {
|
||||
resolve(response);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
console.error("Error saving macros:", response.error);
|
||||
const errorMessage = typeof response.error.data === 'string'
|
||||
const errorMessage =
|
||||
typeof response.error.data === "string"
|
||||
? response.error.data
|
||||
: response.error.message || "Failed to save macros";
|
||||
throw new Error(errorMessage);
|
||||
|
@ -892,5 +933,5 @@ export const useMacrosStore = create<MacrosState>((set, get) => ({
|
|||
} finally {
|
||||
set({ loading: false });
|
||||
}
|
||||
}
|
||||
},
|
||||
}));
|
|
@ -1,6 +1,15 @@
|
|||
import { useEffect, Fragment, useMemo, useState, useCallback } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { LuPenLine, LuCopy, LuMoveRight, LuCornerDownRight, LuArrowUp, LuArrowDown, LuTrash2, LuCommand } from "react-icons/lu";
|
||||
import {
|
||||
LuPenLine,
|
||||
LuCopy,
|
||||
LuMoveRight,
|
||||
LuCornerDownRight,
|
||||
LuArrowUp,
|
||||
LuArrowDown,
|
||||
LuTrash2,
|
||||
LuCommand,
|
||||
} from "react-icons/lu";
|
||||
|
||||
import { KeySequence, useMacrosStore, generateMacroId } from "@/hooks/stores";
|
||||
import { SettingsPageHeader } from "@/components/SettingsPageheader";
|
||||
|
@ -27,9 +36,9 @@ export default function SettingsMacrosRoute() {
|
|||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
const [macroToDelete, setMacroToDelete] = useState<KeySequence | null>(null);
|
||||
|
||||
const isMaxMacrosReached = useMemo(() =>
|
||||
macros.length >= MAX_TOTAL_MACROS,
|
||||
[macros.length]
|
||||
const isMaxMacrosReached = useMemo(
|
||||
() => macros.length >= MAX_TOTAL_MACROS,
|
||||
[macros.length],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -38,7 +47,8 @@ export default function SettingsMacrosRoute() {
|
|||
}
|
||||
}, [initialized, loadMacros]);
|
||||
|
||||
const handleDuplicateMacro = useCallback(async (macro: KeySequence) => {
|
||||
const handleDuplicateMacro = useCallback(
|
||||
async (macro: KeySequence) => {
|
||||
if (!macro?.id || !macro?.name) {
|
||||
notifications.error("Invalid macro data");
|
||||
return;
|
||||
|
@ -70,15 +80,18 @@ export default function SettingsMacrosRoute() {
|
|||
} finally {
|
||||
setActionLoadingId(null);
|
||||
}
|
||||
}, [isMaxMacrosReached, macros, saveMacros, setActionLoadingId]);
|
||||
},
|
||||
[isMaxMacrosReached, macros, saveMacros, setActionLoadingId],
|
||||
);
|
||||
|
||||
const handleMoveMacro = useCallback(async (index: number, direction: 'up' | 'down', macroId: string) => {
|
||||
const handleMoveMacro = useCallback(
|
||||
async (index: number, direction: "up" | "down", macroId: string) => {
|
||||
if (!Array.isArray(macros) || macros.length === 0) {
|
||||
notifications.error("No macros available");
|
||||
return;
|
||||
}
|
||||
|
||||
const newIndex = direction === 'up' ? index - 1 : index + 1;
|
||||
const newIndex = direction === "up" ? index - 1 : index + 1;
|
||||
if (newIndex < 0 || newIndex >= macros.length) return;
|
||||
|
||||
setActionLoadingId(macroId);
|
||||
|
@ -99,14 +112,18 @@ export default function SettingsMacrosRoute() {
|
|||
} finally {
|
||||
setActionLoadingId(null);
|
||||
}
|
||||
}, [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));
|
||||
const updatedMacros = normalizeSortOrders(
|
||||
macros.filter(m => m.id !== macroToDelete.id),
|
||||
);
|
||||
await saveMacros(updatedMacros);
|
||||
notifications.success(`Macro "${macroToDelete.name}" deleted successfully`);
|
||||
setShowDeleteConfirm(false);
|
||||
|
@ -122,16 +139,17 @@ export default function SettingsMacrosRoute() {
|
|||
}
|
||||
}, [macroToDelete, macros, saveMacros]);
|
||||
|
||||
const MacroList = useMemo(() => (
|
||||
const MacroList = useMemo(
|
||||
() => (
|
||||
<div className="space-y-2">
|
||||
{macros.map((macro, index) => (
|
||||
<Card key={macro.id} className="p-2 bg-white dark:bg-slate-800">
|
||||
<Card key={macro.id} className="bg-white p-2 dark:bg-slate-800">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col gap-1 px-2">
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
onClick={() => handleMoveMacro(index, 'up', macro.id)}
|
||||
onClick={() => handleMoveMacro(index, "up", macro.id)}
|
||||
disabled={index === 0 || actionLoadingId === macro.id}
|
||||
LeadingIcon={LuArrowUp}
|
||||
aria-label={`Move ${macro.name} up`}
|
||||
|
@ -139,59 +157,79 @@ export default function SettingsMacrosRoute() {
|
|||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
onClick={() => handleMoveMacro(index, 'down', macro.id)}
|
||||
onClick={() => handleMoveMacro(index, "down", macro.id)}
|
||||
disabled={index === macros.length - 1 || actionLoadingId === macro.id}
|
||||
LeadingIcon={LuArrowDown}
|
||||
aria-label={`Move ${macro.name} down`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0 flex flex-col justify-center ml-2">
|
||||
<div className="ml-2 flex min-w-0 flex-1 flex-col justify-center">
|
||||
<h3 className="truncate text-sm font-semibold text-black dark:text-white">
|
||||
{macro.name}
|
||||
</h3>
|
||||
<p className="mt-1 ml-4 text-xs text-slate-500 dark:text-slate-400 overflow-hidden">
|
||||
<p className="ml-4 mt-1 overflow-hidden text-xs text-slate-500 dark:text-slate-400">
|
||||
<span className="flex flex-col items-start gap-1">
|
||||
{macro.steps.map((step, stepIndex) => {
|
||||
const StepIcon = stepIndex === 0 ? LuMoveRight : LuCornerDownRight;
|
||||
|
||||
return (
|
||||
<span key={stepIndex} className="inline-flex items-center">
|
||||
<StepIcon className="mr-1 text-slate-400 dark:text-slate-500 h-3 w-3 flex-shrink-0" />
|
||||
<span className="bg-slate-50 dark:bg-slate-800 px-2 py-0.5 rounded-md border border-slate-200/50 dark:border-slate-700/50">
|
||||
{(Array.isArray(step.modifiers) && step.modifiers.length > 0) || (Array.isArray(step.keys) && step.keys.length > 0) ? (
|
||||
<StepIcon className="mr-1 h-3 w-3 flex-shrink-0 text-slate-400 dark:text-slate-500" />
|
||||
<span className="rounded-md border border-slate-200/50 bg-slate-50 px-2 py-0.5 dark:border-slate-700/50 dark:bg-slate-800">
|
||||
{(Array.isArray(step.modifiers) &&
|
||||
step.modifiers.length > 0) ||
|
||||
(Array.isArray(step.keys) && step.keys.length > 0) ? (
|
||||
<>
|
||||
{Array.isArray(step.modifiers) && step.modifiers.map((modifier, idx) => (
|
||||
{Array.isArray(step.modifiers) &&
|
||||
step.modifiers.map((modifier, idx) => (
|
||||
<Fragment key={`mod-${idx}`}>
|
||||
<span className="font-medium text-slate-600 dark:text-slate-200">
|
||||
{modifierDisplayMap[modifier] || modifier}
|
||||
</span>
|
||||
{idx < step.modifiers.length - 1 && (
|
||||
<span className="text-slate-400 dark:text-slate-600"> + </span>
|
||||
<span className="text-slate-400 dark:text-slate-600">
|
||||
{" "}
|
||||
+{" "}
|
||||
</span>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
{Array.isArray(step.modifiers) && step.modifiers.length > 0 && Array.isArray(step.keys) && step.keys.length > 0 && (
|
||||
<span className="text-slate-400 dark:text-slate-600"> + </span>
|
||||
{Array.isArray(step.modifiers) &&
|
||||
step.modifiers.length > 0 &&
|
||||
Array.isArray(step.keys) &&
|
||||
step.keys.length > 0 && (
|
||||
<span className="text-slate-400 dark:text-slate-600">
|
||||
{" "}
|
||||
+{" "}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{Array.isArray(step.keys) && step.keys.map((key, idx) => (
|
||||
{Array.isArray(step.keys) &&
|
||||
step.keys.map((key, idx) => (
|
||||
<Fragment key={`key-${idx}`}>
|
||||
<span className="font-medium text-blue-600 dark:text-blue-400">
|
||||
{keyDisplayMap[key] || key}
|
||||
</span>
|
||||
{idx < step.keys.length - 1 && (
|
||||
<span className="text-slate-400 dark:text-slate-600"> + </span>
|
||||
<span className="text-slate-400 dark:text-slate-600">
|
||||
{" "}
|
||||
+{" "}
|
||||
</span>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<span className="font-medium text-slate-500 dark:text-slate-400">Delay only</span>
|
||||
<span className="font-medium text-slate-500 dark:text-slate-400">
|
||||
Delay only
|
||||
</span>
|
||||
)}
|
||||
{step.delay !== DEFAULT_DELAY && (
|
||||
<span className="ml-1 text-slate-400 dark:text-slate-500">({step.delay}ms)</span>
|
||||
<span className="ml-1 text-slate-400 dark:text-slate-500">
|
||||
({step.delay}ms)
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
|
@ -201,7 +239,7 @@ export default function SettingsMacrosRoute() {
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 ml-4">
|
||||
<div className="ml-4 flex items-center gap-1">
|
||||
<Button
|
||||
size="XS"
|
||||
className="text-red-500 dark:text-red-400"
|
||||
|
@ -250,7 +288,19 @@ export default function SettingsMacrosRoute() {
|
|||
isConfirming={actionLoadingId === macroToDelete?.id}
|
||||
/>
|
||||
</div>
|
||||
), [macros, actionLoadingId, showDeleteConfirm, macroToDelete, handleDeleteMacro]);
|
||||
),
|
||||
[
|
||||
macros,
|
||||
showDeleteConfirm,
|
||||
macroToDelete?.name,
|
||||
macroToDelete?.id,
|
||||
actionLoadingId,
|
||||
handleDeleteMacro,
|
||||
handleMoveMacro,
|
||||
handleDuplicateMacro,
|
||||
navigate,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
|
@ -299,7 +349,9 @@ export default function SettingsMacrosRoute() {
|
|||
/>
|
||||
}
|
||||
/>
|
||||
) : MacroList}
|
||||
) : (
|
||||
MacroList
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -15,17 +15,17 @@ import {
|
|||
} from "@/hooks/stores";
|
||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import notifications from "@/notifications";
|
||||
import { Button, LinkButton } from "@components/Button";
|
||||
import { Button } from "@components/Button";
|
||||
import { GridCard } from "@components/Card";
|
||||
import InputField from "@components/InputField";
|
||||
|
||||
import { SettingsPageHeader } from "../components/SettingsPageheader";
|
||||
import { SelectMenuBasic } from "../components/SelectMenuBasic";
|
||||
|
||||
import { SettingsItem } from "./devices.$id.settings";
|
||||
import Fieldset from "../components/Fieldset";
|
||||
import { ConfirmDialog } from "../components/ConfirmDialog";
|
||||
|
||||
import { SettingsItem } from "./devices.$id.settings";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
const defaultNetworkSettings: NetworkSettings = {
|
||||
|
|
Loading…
Reference in New Issue