Skip to content

Commit 19a2bbd

Browse files
authored
Add receiver dirty state management (#5073)
* Add receiver dirty state management * Fixes per review Coderabbitai Refresh and save buttons are now also disabled while isSaving is true, preventing overlapping MSP flows. needReboot is now reset before capturing the snapshot in loadConfig(), so refreshing won't carry over stale reboot state from discarded edits. * Update buttons
1 parent fb5df15 commit 19a2bbd

1 file changed

Lines changed: 68 additions & 3 deletions

File tree

src/components/tabs/ReceiverTab.vue

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -483,9 +483,18 @@
483483
variant="soft"
484484
/>
485485
<UButton :label="$t('receiverButtonBind')" @click="sendBind" v-if="showBindButton" variant="soft" />
486-
<UButton :label="$t('receiverButtonRefresh')" @click="refreshTab" variant="soft" />
487-
<UButton :label="$t('receiverButtonSave')" @click="saveConfig(false)" v-if="!needReboot" />
488-
<UButton :label="$t('receiverButtonSave')" @click="saveConfig(true)" v-else />
486+
<UFieldGroup size="sm" orientation="horizontal" class="flex!">
487+
<UButton @click="saveConfig(needReboot)" :disabled="!dirty || isSaving">
488+
{{ $t("receiverButtonSave") }}
489+
</UButton>
490+
<UDropdownMenu v-slot="{ open }" :items="saveMenuItems" :content="{ align: 'end', side: 'top' }">
491+
<UButton
492+
:icon="open ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'"
493+
:disabled="!dirty || isSaving"
494+
square
495+
/>
496+
</UDropdownMenu>
497+
</UFieldGroup>
489498
</div>
490499
</BaseTab>
491500
</template>
@@ -602,6 +611,58 @@ const features = computed(() => fcStore.features);
602611
603612
const rcDeadbandConfig = computed(() => fcStore.rcDeadbandConfig);
604613
614+
// Dirty state tracking
615+
const savedSnapshot = ref("");
616+
617+
function takeSnapshot() {
618+
return JSON.stringify({
619+
channelMap: channelMapString.value,
620+
rxMode: selectedRxMode.value,
621+
serialrxProvider: rxConfig.value?.serialrx_provider,
622+
rxSpiProtocol: rxConfig.value?.rxSpiProtocol,
623+
elrsModelId: rxConfig.value?.elrsModelId,
624+
stickMin: rxConfig.value?.stick_min,
625+
stickCenter: rxConfig.value?.stick_center,
626+
stickMax: rxConfig.value?.stick_max,
627+
rcSmoothing: rxConfig.value?.rcSmoothing,
628+
rcSmoothingSetpointCutoff: rxConfig.value?.rcSmoothingSetpointCutoff,
629+
rcSmoothingAutoFactor: rxConfig.value?.rcSmoothingAutoFactor,
630+
rcSmoothingThrottleCutoff: rxConfig.value?.rcSmoothingThrottleCutoff,
631+
rcSmoothingAutoFactorThrottle: rxConfig.value?.rcSmoothingAutoFactorThrottle,
632+
rcSmoothingFeedforwardCutoff: rxConfig.value?.rcSmoothingFeedforwardCutoff,
633+
rssiChannel: rssiConfig.value?.channel,
634+
deadband: rcDeadbandConfig.value?.deadband,
635+
yawDeadband: rcDeadbandConfig.value?.yaw_deadband,
636+
deadband3dThrottle: rcDeadbandConfig.value?.deadband3d_throttle,
637+
featureMask: features.value?.features?._featureMask,
638+
elrsBindingPhrase: elrsBindingPhrase.value,
639+
setpointManualMode: setpointManualMode.value,
640+
throttleManualMode: throttleManualMode.value,
641+
feedforwardManualMode: feedforwardManualMode.value,
642+
});
643+
}
644+
645+
const dirty = computed(() => {
646+
return savedSnapshot.value !== "" && takeSnapshot() !== savedSnapshot.value;
647+
});
648+
649+
const saveMenuItems = computed(() => [
650+
[
651+
{
652+
label: t("receiverButtonSave"),
653+
icon: "i-lucide-save",
654+
disabled: !dirty.value || isSaving.value,
655+
onSelect: () => saveConfig(needReboot.value),
656+
},
657+
{
658+
label: t("receiverButtonRefresh"),
659+
icon: "i-lucide-refresh-cw",
660+
disabled: !dirty.value || isSaving.value,
661+
onSelect: refreshTab,
662+
},
663+
],
664+
]);
665+
605666
// Decode HTML entities in translations (some use &lt; etc)
606667
function decodeHtmlEntities(text) {
607668
if (!text) return text;
@@ -968,6 +1029,9 @@ async function loadConfig() {
9681029
if (savedRate?.rx_refresh_rate) {
9691030
refreshRate.value = savedRate.rx_refresh_rate;
9701031
}
1032+
1033+
needReboot.value = false;
1034+
savedSnapshot.value = takeSnapshot();
9711035
} catch (e) {
9721036
console.error("Failed to load Receiver configuration", e);
9731037
}
@@ -1025,6 +1089,7 @@ async function saveConfig(withReboot = false) {
10251089
mspHelper.writeConfiguration(false, resolve);
10261090
});
10271091
gui_log(t("receiverConfigSaved") || "Configuration saved");
1092+
savedSnapshot.value = takeSnapshot();
10281093
}
10291094
10301095
needReboot.value = false;

0 commit comments

Comments
 (0)