fix: validate bluetooth pin is 6 characters.
This commit is contained in:
@@ -13,6 +13,7 @@ import { Controller, type FieldValues } from "react-hook-form";
|
||||
|
||||
export interface SelectFieldProps<T> extends BaseFormBuilderProps<T> {
|
||||
type: "select";
|
||||
selectChange?: (e: string) => void;
|
||||
properties: BaseFormBuilderProps<T>["properties"] & {
|
||||
enumValue: {
|
||||
[s: string]: string | number;
|
||||
@@ -40,7 +41,10 @@ export function SelectInput<T extends FieldValues>({
|
||||
: [];
|
||||
return (
|
||||
<Select
|
||||
onValueChange={(e) => onChange(Number.parseInt(e))}
|
||||
onValueChange={(e) => {
|
||||
if (field.selectChange) field.selectChange(e);
|
||||
onChange(Number.parseInt(e));
|
||||
}}
|
||||
disabled={disabled}
|
||||
value={value?.toString()}
|
||||
{...remainingProperties}
|
||||
|
||||
@@ -1,23 +1,57 @@
|
||||
import { useAppStore } from "@app/core/stores/appStore";
|
||||
import type { BluetoothValidation } from "@app/validation/config/bluetooth.tsx";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { Protobuf } from "@meshtastic/js";
|
||||
import { useState } from "react";
|
||||
|
||||
export const Bluetooth = (): JSX.Element => {
|
||||
export const Bluetooth = () => {
|
||||
const { config, setWorkingConfig } = useDevice();
|
||||
const [bluetoothValidationText, setBluetoothValidationText] =
|
||||
useState<string>();
|
||||
const {
|
||||
hasErrors,
|
||||
getErrorMessage,
|
||||
hasFieldError,
|
||||
addError,
|
||||
removeError,
|
||||
clearErrors,
|
||||
} = useAppStore();
|
||||
|
||||
const [bluetoothPin, setBluetoothPin] = useState(
|
||||
config?.bluetooth?.fixedPin.toString() ?? "",
|
||||
);
|
||||
|
||||
const validateBluetoothPin = (pin: string) => {
|
||||
// if empty show error they need a pin set
|
||||
if (pin === "") {
|
||||
return addError("fixedPin", "Bluetooth Pin is required");
|
||||
}
|
||||
|
||||
// clear any existing errors
|
||||
clearErrors();
|
||||
|
||||
// if it starts with 0 show error
|
||||
if (pin[0] === "0") {
|
||||
return addError("fixedPin", "Bluetooth Pin cannot start with 0");
|
||||
}
|
||||
// if it's not 6 digits show error
|
||||
if (pin.length < 6) {
|
||||
return addError("fixedPin", "Pin must be 6 digits");
|
||||
}
|
||||
|
||||
removeError("fixedPin");
|
||||
};
|
||||
|
||||
const bluetoothPinChangeEvent = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.value[0] === "0") {
|
||||
setBluetoothValidationText("Bluetooth Pin cannot start with 0.");
|
||||
} else {
|
||||
setBluetoothValidationText("");
|
||||
}
|
||||
const numericValue = e.target.value.replace(/\D/g, "").slice(0, 6);
|
||||
setBluetoothPin(numericValue);
|
||||
validateBluetoothPin(numericValue);
|
||||
};
|
||||
|
||||
const onSubmit = (data: BluetoothValidation) => {
|
||||
if (hasErrors()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setWorkingConfig(
|
||||
new Protobuf.Config.Config({
|
||||
payloadVariant: {
|
||||
@@ -48,6 +82,12 @@ export const Bluetooth = (): JSX.Element => {
|
||||
name: "mode",
|
||||
label: "Pairing mode",
|
||||
description: "Pin selection behaviour.",
|
||||
selectChange: (e) => {
|
||||
if (e !== "1") {
|
||||
setBluetoothPin("");
|
||||
removeError("fixedPin");
|
||||
}
|
||||
},
|
||||
disabledBy: [
|
||||
{
|
||||
fieldName: "enabled",
|
||||
@@ -63,7 +103,9 @@ export const Bluetooth = (): JSX.Element => {
|
||||
name: "fixedPin",
|
||||
label: "Pin",
|
||||
description: "Pin to use when pairing",
|
||||
validationText: bluetoothValidationText,
|
||||
validationText: hasFieldError("fixedPin")
|
||||
? getErrorMessage("fixedPin")
|
||||
: "",
|
||||
inputChange: bluetoothPinChangeEvent,
|
||||
disabledBy: [
|
||||
{
|
||||
@@ -77,7 +119,9 @@ export const Bluetooth = (): JSX.Element => {
|
||||
fieldName: "enabled",
|
||||
},
|
||||
],
|
||||
properties: {},
|
||||
properties: {
|
||||
value: bluetoothPin,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -70,7 +70,6 @@ export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
|
||||
onCheckedChange={(checked) => {
|
||||
checked ? setHTTPS(true) : setHTTPS(false);
|
||||
}}
|
||||
|
||||
// label="Use TLS"
|
||||
// description="Description"
|
||||
disabled={
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { cn } from "@app/core/utils/cn.ts";
|
||||
import { AlignLeftIcon, type LucideIcon } from "lucide-react";
|
||||
import Footer from "./UI/Footer";
|
||||
import { Spinner } from "./UI/Spinner";
|
||||
|
||||
export interface PageLayoutProps {
|
||||
label: string;
|
||||
@@ -10,6 +11,8 @@ export interface PageLayoutProps {
|
||||
icon: LucideIcon;
|
||||
iconClasses?: string;
|
||||
onClick: () => void;
|
||||
disabled?: boolean;
|
||||
isLoading?: boolean;
|
||||
}[];
|
||||
}
|
||||
|
||||
@@ -18,7 +21,7 @@ export const PageLayout = ({
|
||||
noPadding,
|
||||
actions,
|
||||
children,
|
||||
}: PageLayoutProps): JSX.Element => {
|
||||
}: PageLayoutProps) => {
|
||||
return (
|
||||
<>
|
||||
<div className="relative flex h-full w-full flex-col">
|
||||
@@ -33,14 +36,22 @@ export const PageLayout = ({
|
||||
<div className="flex w-full items-center">
|
||||
<span className="w-full text-lg font-medium">{label}</span>
|
||||
<div className="flex justify-end space-x-4">
|
||||
{actions?.map((action, index) => (
|
||||
{actions?.map((action) => (
|
||||
<button
|
||||
key={action.icon.displayName}
|
||||
type="button"
|
||||
disabled={action?.disabled}
|
||||
className="transition-all hover:text-accent"
|
||||
onClick={action.onClick}
|
||||
>
|
||||
<action.icon className={action.iconClasses} />
|
||||
{action?.isLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<action.icon
|
||||
className={action.iconClasses}
|
||||
aria-disabled={action.disabled}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { cn } from "@app/core/utils/cn";
|
||||
|
||||
interface SpinnerProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
size?: "sm" | "md" | "lg";
|
||||
}
|
||||
|
||||
const sizeClasses = {
|
||||
sm: "h-4 w-4",
|
||||
md: "h-8 w-8",
|
||||
lg: "h-12 w-12",
|
||||
};
|
||||
|
||||
export function Spinner({ className, size = "md", ...props }: SpinnerProps) {
|
||||
return (
|
||||
<div
|
||||
aria-label="Loading..."
|
||||
className={cn(
|
||||
"flex items-center justify-center fade-in-50 fade-out-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<svg
|
||||
className={cn("animate-spin-slow stroke-current", sizeClasses[size])}
|
||||
role="img"
|
||||
aria-label="Loading spinner"
|
||||
viewBox="0 0 256 256"
|
||||
>
|
||||
<line
|
||||
x1="128"
|
||||
y1="32"
|
||||
x2="128"
|
||||
y2="64"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="24"
|
||||
/>
|
||||
<line
|
||||
x1="195.9"
|
||||
y1="60.1"
|
||||
x2="173.3"
|
||||
y2="82.7"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="24"
|
||||
/>
|
||||
<line
|
||||
x1="224"
|
||||
y1="128"
|
||||
x2="192"
|
||||
y2="128"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="24"
|
||||
/>
|
||||
<line
|
||||
x1="195.9"
|
||||
y1="195.9"
|
||||
x2="173.3"
|
||||
y2="173.3"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="24"
|
||||
/>
|
||||
<line
|
||||
x1="128"
|
||||
y1="224"
|
||||
x2="128"
|
||||
y2="192"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="24"
|
||||
/>
|
||||
<line
|
||||
x1="60.1"
|
||||
y1="195.9"
|
||||
x2="82.7"
|
||||
y2="173.3"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="24"
|
||||
/>
|
||||
<line
|
||||
x1="32"
|
||||
y1="128"
|
||||
x2="64"
|
||||
y2="128"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="24"
|
||||
/>
|
||||
<line
|
||||
x1="60.1"
|
||||
y1="60.1"
|
||||
x2="82.7"
|
||||
y2="82.7"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="24"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user