New channel config (#807)

* Channel config rework

Add staged channel config with tabbed UI, import/export workflow, and global form state refactor

* Improve import dialog config comparison and UI labels

* Review fixes

* Improve state handling

* Fix default filter behaviour

---------

Co-authored-by: philon- <philon-@users.noreply.github.com>
This commit is contained in:
Jeremy Gallant
2025-09-01 09:07:34 -04:00
committed by GitHub
co-authored by philon-
parent 01fa030ef9
commit 1946000d14
42 changed files with 597 additions and 456 deletions
@@ -8,11 +8,6 @@ export interface RasterSource {
tileSize: number;
}
interface ErrorState {
field: string;
message: string;
}
interface AppState {
selectedDeviceId: number;
devices: {
@@ -24,7 +19,6 @@ interface AppState {
nodeNumToBeRemoved: number;
connectDialogOpen: boolean;
nodeNumDetails: number;
errors: ErrorState[];
setRasterSources: (sources: RasterSource[]) => void;
addRasterSource: (source: RasterSource) => void;
@@ -36,15 +30,6 @@ interface AppState {
setNodeNumToBeRemoved: (nodeNum: number) => void;
setConnectDialogOpen: (open: boolean) => void;
setNodeNumDetails: (nodeNum: number) => void;
// Error management
hasErrors: () => boolean;
getErrorMessage: (field: string) => string | undefined;
hasFieldError: (field: string) => boolean;
addError: (field: string, message: string) => void;
removeError: (field: string) => void;
clearErrors: () => void;
setNewErrors: (newErrors: ErrorState[]) => void;
}
export const useAppStore = create<AppState>()((set, get) => ({
@@ -56,7 +41,6 @@ export const useAppStore = create<AppState>()((set, get) => ({
connectDialogOpen: false,
nodeNumToBeRemoved: 0,
nodeNumDetails: 0,
errors: [],
setRasterSources: (sources: RasterSource[]) => {
set(
@@ -114,47 +98,4 @@ export const useAppStore = create<AppState>()((set, get) => ({
set(() => ({
nodeNumDetails: nodeNum,
})),
hasErrors: () => {
const state = get();
return state.errors.length > 0;
},
getErrorMessage: (field: string) => {
const state = get();
return state.errors.find((err) => err.field === field)?.message;
},
hasFieldError: (field: string) => {
const state = get();
return state.errors.some((err) => err.field === field);
},
addError: (field: string, message: string) => {
set(
produce<AppState>((draft) => {
draft.errors = [
...draft.errors.filter((e) => e.field !== field),
{ field, message },
];
}),
);
},
removeError: (field: string) => {
set(
produce<AppState>((draft) => {
draft.errors = draft.errors.filter((e) => e.field !== field);
}),
);
},
clearErrors: () => {
set(
produce<AppState>((draft) => {
draft.errors = [];
}),
);
},
setNewErrors: (newErrors: ErrorState[]) => {
set(
produce<AppState>((draft) => {
draft.errors = newErrors;
}),
);
},
}));
@@ -20,6 +20,7 @@ export const mockDeviceStore: Device = {
moduleConfig: {} as Protobuf.LocalOnly.LocalModuleConfig,
workingConfig: [],
workingModuleConfig: [],
workingChannelConfig: [],
hardware: {} as Protobuf.Mesh.MyNodeInfo,
metadata: new Map(),
traceroutes: new Map(),
@@ -57,6 +58,9 @@ export const mockDeviceStore: Device = {
removeWorkingModuleConfig: vi.fn(),
getEffectiveConfig: vi.fn(),
getEffectiveModuleConfig: vi.fn(),
setWorkingChannelConfig: vi.fn(),
getWorkingChannelConfig: vi.fn(),
removeWorkingChannelConfig: vi.fn(),
setHardware: vi.fn(),
setActiveNode: vi.fn(),
setPendingSettingsChanges: vi.fn(),
@@ -30,6 +30,7 @@ export interface Device {
moduleConfig: Protobuf.LocalOnly.LocalModuleConfig;
workingConfig: Protobuf.Config.Config[];
workingModuleConfig: Protobuf.ModuleConfig.ModuleConfig[];
workingChannelConfig: Protobuf.Channel.Channel[];
hardware: Protobuf.Mesh.MyNodeInfo;
metadata: Map<number, Protobuf.Mesh.DeviceMetadata>;
traceroutes: Map<
@@ -85,6 +86,11 @@ export interface Device {
getEffectiveModuleConfig<K extends ValidModuleConfigType>(
payloadVariant: K,
): Protobuf.LocalOnly.LocalModuleConfig[K] | undefined;
setWorkingChannelConfig: (channelNum: Protobuf.Channel.Channel) => void;
getWorkingChannelConfig: (
index: Types.ChannelNumber,
) => Protobuf.Channel.Channel | undefined;
removeWorkingChannelConfig: (channelNum?: Types.ChannelNumber) => void;
setHardware: (hardware: Protobuf.Mesh.MyNodeInfo) => void;
setActiveNode: (node: number) => void;
setPendingSettingsChanges: (state: boolean) => void;
@@ -139,6 +145,7 @@ export const useDeviceStore = createStore<PrivateDeviceState>((set, get) => ({
moduleConfig: create(Protobuf.LocalOnly.LocalModuleConfigSchema),
workingConfig: [],
workingModuleConfig: [],
workingChannelConfig: [],
hardware: create(Protobuf.Mesh.MyNodeInfoSchema),
metadata: new Map(),
traceroutes: new Map(),
@@ -443,6 +450,61 @@ export const useDeviceStore = createStore<PrivateDeviceState>((set, get) => ({
};
},
setWorkingChannelConfig: (config: Protobuf.Channel.Channel) => {
set(
produce<PrivateDeviceState>((draft) => {
const device = draft.devices.get(id);
if (!device) {
return;
}
const index = device.workingChannelConfig.findIndex(
(wcc) => wcc.index === config.index,
);
if (index !== -1) {
device.workingChannelConfig[index] = config;
} else {
device.workingChannelConfig.push(config);
}
}),
);
},
getWorkingChannelConfig: (channelNum: Types.ChannelNumber) => {
const device = get().devices.get(id);
if (!device) {
return;
}
const workingChannelConfig = device.workingChannelConfig.find(
(c) => c.index === channelNum,
);
return workingChannelConfig;
},
removeWorkingChannelConfig: (channelNum?: Types.ChannelNumber) => {
set(
produce<PrivateDeviceState>((draft) => {
const device = draft.devices.get(id);
if (!device) {
return;
}
if (channelNum === undefined) {
device.workingChannelConfig = [];
return;
}
const index = device.workingChannelConfig.findIndex(
(wcc: Protobuf.Channel.Channel) => wcc.index === channelNum,
);
if (index !== -1) {
device.workingChannelConfig.splice(index, 1);
}
}),
);
},
setHardware: (hardware: Protobuf.Mesh.MyNodeInfo) => {
set(
produce<PrivateDeviceState>((draft) => {