feat: add fixed position coordinate picker (#909)

* chore: remove unused logo SVG files

* feat: add interactive fixed position picker with map interface

- Created new FixedPositionPicker component with clickable map for setting device coordinates
- Added form field type for fixed position picker that appears when fixedPosition toggle is enabled
- Implemented position request functionality to retrieve current device location

* feat: display altitude unit based on user's display settings

- Added dynamic altitude unit (Meters/Feet) that respects the user's imperial/metric display preference
- Updated altitude field description to show the appropriate unit instead of hardcoded "Meters"

* refactor: replace any type with MapLayerMouseEvent in map click handler

* refactor: improve accessibility and code quality in FixedPositionPicker

- Replace hardcoded IDs with useId() hook for proper accessibility
- Use Number.isNaN() instead of isNaN() for more reliable type checking
- Add radix parameter to parseInt() and remove unnecessary fragment wrapper

* refactor: simplify fixed position picker integration

- Removed dedicated FixedPositionPicker form field type in favor of toggle's additionalContent prop
- Moved FixedPositionPicker to render conditionally within toggle field instead of as separate dynamic field
- Streamlined form field types by eliminating FixedPositionPickerFieldProps

* style: format code with consistent line breaks and import ordering

* refactor: simplify fixed position picker container styling

* feat: disable fixed position toggle when GPS is enabled

* refactor: use ComponentRef instead of ElementRef in Switch component

* refactor: replace interactive map picker with inline coordinate fields for fixed position

- Removed FixedPositionPicker component with map interface
- Added latitude, longitude, and altitude fields directly to position form
- Moved coordinate validation into PositionValidationSchema with proper min/max bounds
- Updated translation strings to include coordinate ranges and improved altitude description
- Coordinates now sent via setFixedPosition admin message on form submit when fixedPosition is enabled

* refactor: simplify toggle field by removing additionalContent prop and unused field spreading

- Removed additionalContent prop and its JSDoc documentation from ToggleFieldProps
- Removed rendering of additionalContent below toggle switch
- Cleaned up Controller render function by removing unused rest spread operator
- Renamed field destructuring to controllerField for clarity

* refactor: improve fixed position handling and add position broadcast request

- Restructure onSubmit to save config before sending admin message
- Add position broadcast request after setting fixed position to immediately update display
- Add comprehensive debug logging throughout submission flow
- Extract coordinate exclusion logic earlier in submission process for clarity
- Add 1 second delay before requesting position broadcast to allow fixed position processing

* feat: add max length constraint to latitude and longitude fields

- Set fieldLength.max to 10 for both latitude and longitude inputs
- Prevents excessive decimal precision while maintaining 7 decimal places (±1.1cm accuracy)
This commit is contained in:
Kamil Dzieniszewski
2025-11-27 15:40:57 -05:00
committed by GitHub
parent 5debdb4689
commit 6a7be99a6a
11 changed files with 299 additions and 120 deletions
+23 -2
View File
@@ -1,4 +1,5 @@
import { deviceRoute, moduleRoute, radioRoute } from "@app/routes";
import { toBinary } from "@bufbuild/protobuf";
import { PageLayout } from "@components/PageLayout.tsx";
import { Sidebar } from "@components/Sidebar.tsx";
import { SidebarButton } from "@components/UI/Sidebar/SidebarButton.tsx";
@@ -6,6 +7,7 @@ import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.tsx";
import { useToast } from "@core/hooks/useToast.ts";
import { useDevice } from "@core/stores";
import { cn } from "@core/utils/cn.ts";
import { Protobuf } from "@meshtastic/core";
import { DeviceConfig } from "@pages/Settings/DeviceConfig.tsx";
import { ModuleConfig } from "@pages/Settings/ModuleConfig.tsx";
import { useNavigate, useRouterState } from "@tanstack/react-router";
@@ -27,6 +29,7 @@ const ConfigPage = () => {
getAllConfigChanges,
getAllModuleConfigChanges,
getAllChannelChanges,
getAllQueuedAdminMessages,
connection,
clearAllChanges,
setConfig,
@@ -35,6 +38,7 @@ const ConfigPage = () => {
getConfigChangeCount,
getModuleConfigChangeCount,
getChannelChangeCount,
getAdminMessageChangeCount,
} = useDevice();
const [isSaving, setIsSaving] = useState(false);
@@ -49,6 +53,7 @@ const ConfigPage = () => {
const configChangeCount = getConfigChangeCount();
const moduleConfigChangeCount = getModuleConfigChangeCount();
const channelChangeCount = getChannelChangeCount();
const adminMessageChangeCount = getAdminMessageChangeCount();
const sections = useMemo(
() => [
@@ -121,6 +126,7 @@ const ConfigPage = () => {
const channelChanges = getAllChannelChanges();
const configChanges = getAllConfigChanges();
const moduleConfigChanges = getAllModuleConfigChanges();
const adminMessages = getAllQueuedAdminMessages();
await Promise.all(
channelChanges.map((channel) =>
@@ -165,6 +171,19 @@ const ConfigPage = () => {
await connection?.commitEditSettings();
}
// Send queued admin messages after configs are committed
if (adminMessages.length > 0) {
await Promise.all(
adminMessages.map((message) =>
connection?.sendPacket(
toBinary(Protobuf.Admin.AdminMessageSchema, message),
Protobuf.Portnums.PortNum.ADMIN_APP,
"self",
),
),
);
}
channelChanges.forEach((newChannel) => {
addChannel(newChannel);
});
@@ -206,6 +225,7 @@ const ConfigPage = () => {
connection,
getAllModuleConfigChanges,
getAllChannelChanges,
getAllQueuedAdminMessages,
formMethods,
addChannel,
setConfig,
@@ -244,7 +264,8 @@ const ConfigPage = () => {
const hasDrafts =
getConfigChangeCount() > 0 ||
getModuleConfigChangeCount() > 0 ||
getChannelChangeCount() > 0;
getChannelChangeCount() > 0 ||
adminMessageChangeCount > 0;
const hasPending = hasDrafts || rhfState.isDirty;
const buttonOpacity = hasPending ? "opacity-100" : "opacity-0";
const saveDisabled = isSaving || !rhfState.isValid || !hasPending;
@@ -312,7 +333,7 @@ const ConfigPage = () => {
label={activeSection?.label ?? ""}
actions={actions}
>
<ActiveComponent onFormInit={onFormInit} />
{ActiveComponent && <ActiveComponent onFormInit={onFormInit} />}
</PageLayout>
);
};