Squashed 'sample-front-end/' changes from 99ea3a6..9f02555

9f02555 feat(ui): Improve schedule dirty state detection

git-subtree-dir: sample-front-end
git-subtree-split: 9f02555bbcc1e7bd576ad04763fbeb5d1f0e0b31
This commit is contained in:
2025-11-29 10:52:05 +09:00
parent 06e49be1f9
commit 305743d752
+41 -27
View File
@@ -1,3 +1,4 @@
import React, { useState, useRef, useEffect } from 'react'; import React, { useState, useRef, useEffect } from 'react';
import { SyncStrategy, RegexReplacement, SyncState, ScheduleSettings, ScheduleMode } from '../types'; import { SyncStrategy, RegexReplacement, SyncState, ScheduleSettings, ScheduleMode } from '../types';
import { import {
@@ -61,6 +62,25 @@ const STRATEGIES: StrategyOption[] = [
const WEEK_DAYS = ['S', 'M', 'T', 'W', 'T', 'F', 'S']; const WEEK_DAYS = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
// Helper to determine the actual mode and settings that would be saved based on the current UI state
const deriveEffectiveSchedule = (schedule: ScheduleSettings, tab: ScheduleMode): ScheduleSettings => {
const derived = { ...schedule };
if (tab === ScheduleMode.CRON) {
derived.mode = derived.cronExpression.trim() !== '' ? ScheduleMode.CRON : ScheduleMode.DISABLED;
} else {
// For Daily/Weekly
// If the mode matches the tab, we keep it (Enabled).
// If the mode doesn't match (e.g. it was CRON or DISABLED), then in the context of this tab, it is effectively Disabled until the user checks the box.
if (derived.mode === tab) {
derived.mode = tab;
} else {
derived.mode = ScheduleMode.DISABLED;
}
}
return derived;
};
interface StrategySelectorProps { interface StrategySelectorProps {
currentStrategy: SyncStrategy; currentStrategy: SyncStrategy;
onSelect: (strategy: SyncStrategy, label: string) => void; onSelect: (strategy: SyncStrategy, label: string) => void;
@@ -123,10 +143,13 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
setIsRegexDirty(isDifferent); setIsRegexDirty(isDifferent);
}, [localReplacements, savedRegexReplacements]); }, [localReplacements, savedRegexReplacements]);
// Check dirty state for Schedule (including Active Tab changes)
useEffect(() => { useEffect(() => {
const isDifferent = JSON.stringify(localSchedule) !== JSON.stringify(savedSchedule); // We calculate what the "effective" schedule would be if we saved right now.
const effectiveLocal = deriveEffectiveSchedule(localSchedule, activeTab);
const isDifferent = JSON.stringify(effectiveLocal) !== JSON.stringify(savedSchedule);
setIsScheduleDirty(isDifferent); setIsScheduleDirty(isDifferent);
}, [localSchedule, savedSchedule]); }, [localSchedule, savedSchedule, activeTab]);
const selectedOption = STRATEGIES.find(s => s.value === currentStrategy) || STRATEGIES[0]; const selectedOption = STRATEGIES.find(s => s.value === currentStrategy) || STRATEGIES[0];
@@ -140,6 +163,11 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
return () => document.removeEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside);
}, []); }, []);
// Determine if tabs have changed from the saved state
const initialTab = savedSchedule.mode === ScheduleMode.DISABLED ? ScheduleMode.CRON : savedSchedule.mode;
const hasTabChanged = activeTab !== initialTab;
const isScheduleActionable = isScheduleDirty || hasTabChanged;
const handleSelect = (strategy: StrategyOption) => { const handleSelect = (strategy: StrategyOption) => {
if (isLocked) return; if (isLocked) return;
onSelect(strategy.value, strategy.label); onSelect(strategy.value, strategy.label);
@@ -196,37 +224,23 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
setLocalSchedule(JSON.parse(JSON.stringify(savedSchedule))); setLocalSchedule(JSON.parse(JSON.stringify(savedSchedule)));
if (savedSchedule.mode !== ScheduleMode.DISABLED) { if (savedSchedule.mode !== ScheduleMode.DISABLED) {
setActiveTab(savedSchedule.mode); setActiveTab(savedSchedule.mode);
} else {
setActiveTab(ScheduleMode.CRON);
} }
}; };
const handleSaveScheduleClick = async () => { const handleSaveScheduleClick = async () => {
if (isLocked) return; if (isLocked) return;
let settingsToSave = { ...localSchedule }; // Determine the effective settings based on the current view (tab) and inputs
const settingsToSave = deriveEffectiveSchedule(localSchedule, activeTab);
// Logic to determine mode based on active Tab and checkbox state
if (activeTab === ScheduleMode.CRON) {
if (settingsToSave.cronExpression.trim() !== '') {
settingsToSave.mode = ScheduleMode.CRON;
} else {
// Empty cron -> disabled
settingsToSave.mode = ScheduleMode.DISABLED;
}
}
// For Daily/Weekly, enforce: Save commits what is seen in the active tab.
if (activeTab !== ScheduleMode.CRON) {
// If the mode matches the active tab, it's enabled. Otherwise disabled.
if (localSchedule.mode !== activeTab) {
settingsToSave.mode = ScheduleMode.DISABLED;
}
}
// Call API // Call API
const success = await onSaveSchedule(settingsToSave); const success = await onSaveSchedule(settingsToSave);
if (success) { if (success) {
setLocalSchedule(settingsToSave); setLocalSchedule(settingsToSave);
setIsScheduleDirty(false); // Dirty state is cleared by the useEffect prop update, or we can clear it optimistically here if needed,
// but useEffect [savedSchedule] handles it correctly.
} }
}; };
@@ -554,9 +568,9 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
<div className="flex items-center gap-2 justify-end pt-3 border-t border-white/5"> <div className="flex items-center gap-2 justify-end pt-3 border-t border-white/5">
<button <button
onClick={handleResetSchedule} onClick={handleResetSchedule}
disabled={!isScheduleDirty} disabled={!isScheduleActionable}
className={`flex items-center justify-center space-x-1.5 px-3 py-1.5 rounded-md text-xs font-medium border transition-all className={`flex items-center justify-center space-x-1.5 px-3 py-1.5 rounded-md text-xs font-medium border transition-all
${isScheduleDirty ${isScheduleActionable
? 'bg-gray-800 border-gray-600 text-gray-300 hover:bg-gray-700 hover:text-white' ? 'bg-gray-800 border-gray-600 text-gray-300 hover:bg-gray-700 hover:text-white'
: 'bg-transparent border-transparent text-gray-700 cursor-not-allowed'}`} : 'bg-transparent border-transparent text-gray-700 cursor-not-allowed'}`}
> >
@@ -565,9 +579,9 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
</button> </button>
<button <button
onClick={handleSaveScheduleClick} onClick={handleSaveScheduleClick}
disabled={!isScheduleDirty} disabled={!isScheduleActionable}
className={`flex items-center justify-center space-x-1.5 px-3 py-1.5 rounded-md text-xs font-bold border transition-all className={`flex items-center justify-center space-x-1.5 px-3 py-1.5 rounded-md text-xs font-bold border transition-all
${isScheduleDirty ${isScheduleActionable
? 'bg-plex-orange border-plex-orange text-gray-900 hover:bg-yellow-500 shadow-lg shadow-plex-orange/10' ? 'bg-plex-orange border-plex-orange text-gray-900 hover:bg-yellow-500 shadow-lg shadow-plex-orange/10'
: 'bg-gray-800/30 border-gray-800/50 text-gray-600 cursor-not-allowed'}`} : 'bg-gray-800/30 border-gray-800/50 text-gray-600 cursor-not-allowed'}`}
> >
@@ -614,4 +628,4 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
); );
}; };
export default StrategySelector; export default StrategySelector;