diff --git a/apps/web/src/components/Form/FormInput.tsx b/apps/web/src/components/Form/FormInput.tsx index 487f96569..aa0d6f3f8 100644 --- a/apps/web/src/components/Form/FormInput.tsx +++ b/apps/web/src/components/Form/FormInput.tsx @@ -59,10 +59,20 @@ export function GenericInput({ field.inputChange(e); } + if (field.type !== "number") { + controllerField.onChange(newValue); + return; + } + + // Preserve intermediate values while typing negative numbers or decimals. + if (newValue === "" || newValue === "-" || newValue.endsWith(".")) { + controllerField.onChange(newValue); + return; + } + + const parsed = Number.parseFloat(newValue); controllerField.onChange( - field.type === "number" - ? Number.parseFloat(newValue).toString() - : newValue, + Number.isNaN(parsed) ? newValue : parsed.toString(), ); }; diff --git a/apps/web/src/components/PageComponents/Settings/Position.tsx b/apps/web/src/components/PageComponents/Settings/Position.tsx index c61674695..4483b2620 100644 --- a/apps/web/src/components/PageComponents/Settings/Position.tsx +++ b/apps/web/src/components/PageComponents/Settings/Position.tsx @@ -236,7 +236,7 @@ export const Position = ({ onFormInit }: PositionConfigProps) => { properties: { step: 0.0000001, suffix: "Degrees", - fieldLength: { max: 10 }, + fieldLength: { max: 11 }, }, disabledBy: [{ fieldName: "fixedPosition" }], }, @@ -248,7 +248,7 @@ export const Position = ({ onFormInit }: PositionConfigProps) => { properties: { step: 0.0000001, suffix: "Degrees", - fieldLength: { max: 10 }, + fieldLength: { max: 12 }, }, disabledBy: [{ fieldName: "fixedPosition" }], }, diff --git a/apps/web/src/validation/config/position.test.ts b/apps/web/src/validation/config/position.test.ts new file mode 100644 index 000000000..56fc5faec --- /dev/null +++ b/apps/web/src/validation/config/position.test.ts @@ -0,0 +1,68 @@ +import { describe, expect, it } from "vitest"; +import { PositionValidationSchema } from "./position.ts"; + +const validBase = { + positionBroadcastSecs: 0, + positionBroadcastSmartEnabled: false, + fixedPosition: false, + gpsUpdateInterval: 0, + positionFlags: 0, + rxGpio: 0, + txGpio: 0, + broadcastSmartMinimumDistance: 0, + broadcastSmartMinimumIntervalSecs: 0, + gpsEnGpio: 0, + gpsMode: 0, +}; + +describe("PositionValidationSchema", () => { + it("accepts positive latitude and longitude with 7 decimal places", () => { + const result = PositionValidationSchema.safeParse({ + ...validBase, + latitude: 34.1147648, + longitude: 28.3166667, + }); + expect(result.success).toBe(true); + }); + + it("accepts negative latitude and longitude with 7 decimal places", () => { + const result = PositionValidationSchema.safeParse({ + ...validBase, + latitude: -34.1147648, + longitude: -122.4194165, + }); + expect(result.success).toBe(true); + }); + + it("rejects latitude with more than 7 decimal places", () => { + const result = PositionValidationSchema.safeParse({ + ...validBase, + latitude: -34.11476481, + }); + expect(result.success).toBe(false); + }); + + it("rejects longitude with more than 7 decimal places", () => { + const result = PositionValidationSchema.safeParse({ + ...validBase, + longitude: -122.41941654, + }); + expect(result.success).toBe(false); + }); + + it("rejects latitude outside the valid range", () => { + const result = PositionValidationSchema.safeParse({ + ...validBase, + latitude: 91, + }); + expect(result.success).toBe(false); + }); + + it("rejects longitude outside the valid range", () => { + const result = PositionValidationSchema.safeParse({ + ...validBase, + longitude: -181, + }); + expect(result.success).toBe(false); + }); +}); diff --git a/apps/web/src/validation/config/position.ts b/apps/web/src/validation/config/position.ts index 33419b60f..4a994315a 100644 --- a/apps/web/src/validation/config/position.ts +++ b/apps/web/src/validation/config/position.ts @@ -3,6 +3,12 @@ import { z } from "zod/v4"; const GpsModeEnum = z.enum(Protobuf.Config.Config_PositionConfig_GpsMode); +const maxDecimalPlaces = (places: number) => (value: number | undefined) => { + if (value === undefined) return true; + const [, decimals] = value.toString().split("."); + return !decimals || decimals.length <= places; +}; + export const PositionValidationSchema = z.object({ positionBroadcastSecs: z.coerce.number().int().min(0), positionBroadcastSmartEnabled: z.boolean(), @@ -15,8 +21,18 @@ export const PositionValidationSchema = z.object({ broadcastSmartMinimumIntervalSecs: z.coerce.number().int().min(0), gpsEnGpio: z.coerce.number().int().min(0), gpsMode: GpsModeEnum, - latitude: z.coerce.number().min(-90).max(90).optional(), - longitude: z.coerce.number().min(-180).max(180).optional(), + latitude: z.coerce + .number() + .min(-90) + .max(90) + .optional() + .refine(maxDecimalPlaces(7), { message: "Max 7 decimal precision" }), + longitude: z.coerce + .number() + .min(-180) + .max(180) + .optional() + .refine(maxDecimalPlaces(7), { message: "Max 7 decimal precision" }), altitude: z.coerce.number().optional(), });