feat(modal): closeIfPristine save-guard + per-editor adoption
Modal gains closeIfPristine(entityId): when editing an existing entity and no tracked field has changed, the helper force-closes the modal silently and returns true so the caller can skip the PUT and the misleading "updated" toast. Each editor's save handler now early-returns on the no-op edit path: advanced-calibration, assets, audio-processing-templates, audio-sources, calibration, devices, game-integration, ha-light-targets, home-assistant-sources, mqtt-sources, pattern-templates, scene-presets, sync-clocks, targets, weather-sources.
This commit is contained in:
@@ -156,6 +156,24 @@ export class Modal {
|
||||
return Object.keys(this._initialValues).some(k => this._initialValues[k] !== cur[k]);
|
||||
}
|
||||
|
||||
/**
|
||||
* No-op save guard for edit modals. When `entityId` is truthy (we are
|
||||
* editing an existing entity), snapshot tracking is configured, and no
|
||||
* tracked field has changed, force-close the modal silently and return
|
||||
* `true` so the caller can early-return — skipping the network request
|
||||
* and the misleading "updated" toast.
|
||||
*
|
||||
* Returns `false` when the save flow must continue (create flow, no
|
||||
* snapshot taken, or at least one tracked field changed).
|
||||
*/
|
||||
closeIfPristine(entityId: unknown): boolean {
|
||||
if (!entityId) return false;
|
||||
if (Object.keys(this._initialValues).length === 0) return false;
|
||||
if (this.isDirty()) return false;
|
||||
this.forceClose();
|
||||
return true;
|
||||
}
|
||||
|
||||
showError(msg: string) {
|
||||
if (this.errorEl) {
|
||||
this.errorEl.textContent = msg;
|
||||
|
||||
@@ -199,6 +199,8 @@ export async function saveAdvancedCalibration(): Promise<void> {
|
||||
const cssId = _state.cssId;
|
||||
if (!cssId) return;
|
||||
|
||||
if (_modal.closeIfPristine(cssId)) return;
|
||||
|
||||
if (_state.lines.length === 0) {
|
||||
showToast(t('calibration.advanced.no_lines_warning') || 'Add at least one line', 'error');
|
||||
return;
|
||||
|
||||
@@ -380,6 +380,8 @@ export async function showAssetEditor(editId: string): Promise<void> {
|
||||
|
||||
export async function saveAssetMetadata(): Promise<void> {
|
||||
const id = (document.getElementById('asset-editor-id') as HTMLInputElement).value;
|
||||
if (assetEditorModal.closeIfPristine(id)) return;
|
||||
|
||||
const name = (document.getElementById('asset-editor-name') as HTMLInputElement).value.trim();
|
||||
const description = (document.getElementById('asset-editor-description') as HTMLInputElement).value.trim();
|
||||
const errorEl = document.getElementById('asset-editor-error')!;
|
||||
|
||||
@@ -194,6 +194,8 @@ export async function editAudioProcessingTemplate(templateId: string) {
|
||||
|
||||
export async function saveAudioProcessingTemplate() {
|
||||
const templateId = (document.getElementById('apt-id') as HTMLInputElement).value;
|
||||
if (aptModal.closeIfPristine(templateId)) return;
|
||||
|
||||
const name = (document.getElementById('apt-name') as HTMLInputElement).value.trim();
|
||||
const description = (document.getElementById('apt-description') as HTMLInputElement).value.trim();
|
||||
|
||||
|
||||
@@ -153,6 +153,8 @@ export function onAudioSourceTypeChange() {
|
||||
|
||||
export async function saveAudioSource() {
|
||||
const id = (document.getElementById('audio-source-id') as HTMLInputElement).value;
|
||||
if (audioSourceModal.closeIfPristine(id)) return;
|
||||
|
||||
const name = (document.getElementById('audio-source-name') as HTMLInputElement).value.trim();
|
||||
const sourceType = (document.getElementById('audio-source-type') as HTMLSelectElement).value;
|
||||
const description = (document.getElementById('audio-source-description') as HTMLInputElement).value.trim() || null;
|
||||
|
||||
@@ -914,6 +914,8 @@ export async function saveCalibration() {
|
||||
const cssId = (document.getElementById('calibration-css-id') as HTMLInputElement).value;
|
||||
const error = document.getElementById('calibration-error') as HTMLElement;
|
||||
|
||||
if (calibModal.closeIfPristine(cssMode ? cssId : deviceId)) return;
|
||||
|
||||
if (cssMode) {
|
||||
await _clearCSSTestMode();
|
||||
} else {
|
||||
|
||||
@@ -843,6 +843,8 @@ export function closeDeviceSettingsModal() { settingsModal.close(); }
|
||||
|
||||
export async function saveDeviceSettings() {
|
||||
const deviceId = (document.getElementById('settings-device-id') as HTMLInputElement).value;
|
||||
if (settingsModal.closeIfPristine(deviceId)) return;
|
||||
|
||||
const name = (document.getElementById('settings-device-name') as HTMLInputElement).value.trim();
|
||||
const url = settingsModal._getUrl();
|
||||
|
||||
|
||||
@@ -707,6 +707,8 @@ export async function showGameIntegrationEditor(editId: string | null = null) {
|
||||
|
||||
export async function saveGameIntegration() {
|
||||
const id = (document.getElementById('gi-id') as HTMLInputElement).value;
|
||||
if (giModal.closeIfPristine(id)) return;
|
||||
|
||||
const name = (document.getElementById('gi-name') as HTMLInputElement).value.trim();
|
||||
if (!name) { giModal.showError(t('game_integration.error.name_required')); return; }
|
||||
|
||||
|
||||
@@ -487,6 +487,8 @@ export async function closeHALightEditor(): Promise<void> {
|
||||
|
||||
export async function saveHALightEditor(): Promise<void> {
|
||||
const targetId = (document.getElementById('ha-light-editor-id') as HTMLInputElement).value;
|
||||
if (haLightEditorModal.closeIfPristine(targetId)) return;
|
||||
|
||||
const name = (document.getElementById('ha-light-editor-name') as HTMLInputElement).value.trim();
|
||||
const haSourceId = (document.getElementById('ha-light-editor-ha-source') as HTMLSelectElement).value;
|
||||
const colorSourceRaw = (document.getElementById('ha-light-editor-css-source') as HTMLSelectElement).value;
|
||||
|
||||
@@ -142,6 +142,8 @@ export async function closeHASourceModal(): Promise<void> {
|
||||
|
||||
export async function saveHASource(): Promise<void> {
|
||||
const id = (document.getElementById('ha-source-id') as HTMLInputElement).value;
|
||||
if (haSourceModal.closeIfPristine(id)) return;
|
||||
|
||||
const name = (document.getElementById('ha-source-name') as HTMLInputElement).value.trim();
|
||||
const host = (document.getElementById('ha-source-host') as HTMLInputElement).value.trim();
|
||||
const token = (document.getElementById('ha-source-token') as HTMLInputElement).value.trim();
|
||||
|
||||
@@ -115,6 +115,8 @@ export async function closeMQTTSourceModal(): Promise<void> {
|
||||
|
||||
export async function saveMQTTSource(): Promise<void> {
|
||||
const id = (document.getElementById('mqtt-source-id') as HTMLInputElement).value;
|
||||
if (mqttSourceModal.closeIfPristine(id)) return;
|
||||
|
||||
const name = (document.getElementById('mqtt-source-name') as HTMLInputElement).value.trim();
|
||||
const broker_host = (document.getElementById('mqtt-source-host') as HTMLInputElement).value.trim();
|
||||
const broker_port = parseInt((document.getElementById('mqtt-source-port') as HTMLInputElement).value, 10) || 1883;
|
||||
|
||||
@@ -240,6 +240,8 @@ export async function savePatternTemplate(): Promise<void> {
|
||||
}
|
||||
|
||||
const templateId = (document.getElementById('pattern-template-id') as HTMLInputElement).value;
|
||||
if (patternModal.closeIfPristine(templateId)) return;
|
||||
|
||||
const name = (document.getElementById('pattern-template-name') as HTMLInputElement).value.trim();
|
||||
const description = (document.getElementById('pattern-template-description') as HTMLInputElement).value.trim();
|
||||
|
||||
|
||||
@@ -369,6 +369,8 @@ export async function editScenePreset(presetId: string): Promise<void> {
|
||||
// ===== Save (create or update) =====
|
||||
|
||||
export async function saveScenePreset(): Promise<void> {
|
||||
if (scenePresetModal.closeIfPristine(_editingId)) return;
|
||||
|
||||
const name = (document.getElementById('scene-preset-editor-name') as HTMLInputElement).value.trim();
|
||||
const description = (document.getElementById('scene-preset-editor-description') as HTMLInputElement).value.trim();
|
||||
const errorEl = document.getElementById('scene-preset-editor-error')!;
|
||||
|
||||
@@ -110,6 +110,8 @@ export async function closeSyncClockModal(): Promise<void> {
|
||||
|
||||
export async function saveSyncClock(): Promise<void> {
|
||||
const id = (document.getElementById('sync-clock-id') as HTMLInputElement).value;
|
||||
if (syncClockModal.closeIfPristine(id)) return;
|
||||
|
||||
const name = (document.getElementById('sync-clock-name') as HTMLInputElement).value.trim();
|
||||
const speed = parseFloat((document.getElementById('sync-clock-speed') as HTMLInputElement).value);
|
||||
const description = (document.getElementById('sync-clock-description') as HTMLInputElement).value.trim() || null;
|
||||
|
||||
@@ -496,6 +496,8 @@ export function forceCloseTargetEditorModal() {
|
||||
|
||||
export async function saveTargetEditor() {
|
||||
const targetId = (document.getElementById('target-editor-id') as HTMLInputElement).value;
|
||||
if (targetEditorModal.closeIfPristine(targetId)) return;
|
||||
|
||||
const name = (document.getElementById('target-editor-name') as HTMLInputElement).value.trim();
|
||||
const deviceId = (document.getElementById('target-editor-device') as HTMLSelectElement).value;
|
||||
const standbyInterval = parseFloat((document.getElementById('target-editor-keepalive-interval') as HTMLInputElement).value);
|
||||
|
||||
@@ -146,6 +146,8 @@ export async function closeWeatherSourceModal(): Promise<void> {
|
||||
|
||||
export async function saveWeatherSource(): Promise<void> {
|
||||
const id = (document.getElementById('weather-source-id') as HTMLInputElement).value;
|
||||
if (weatherSourceModal.closeIfPristine(id)) return;
|
||||
|
||||
const name = (document.getElementById('weather-source-name') as HTMLInputElement).value.trim();
|
||||
const provider = (document.getElementById('weather-source-provider') as HTMLSelectElement).value;
|
||||
const latitude = parseFloat((document.getElementById('weather-source-latitude') as HTMLInputElement).value) || 50.0;
|
||||
|
||||
Reference in New Issue
Block a user