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 { SyncStrategy, RegexReplacement, SyncState, ScheduleSettings, ScheduleMode } from '../types';
import {
@@ -61,6 +62,25 @@ const STRATEGIES: StrategyOption[] = [
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 {
currentStrategy: SyncStrategy;
onSelect: (strategy: SyncStrategy, label: string) => void;
@@ -123,10 +143,13 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
setIsRegexDirty(isDifferent);
}, [localReplacements, savedRegexReplacements]);
// Check dirty state for Schedule (including Active Tab changes)
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);
}, [localSchedule, savedSchedule]);
}, [localSchedule, savedSchedule, activeTab]);
const selectedOption = STRATEGIES.find(s => s.value === currentStrategy) || STRATEGIES[0];
@@ -140,6 +163,11 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
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) => {
if (isLocked) return;
onSelect(strategy.value, strategy.label);
@@ -196,37 +224,23 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
setLocalSchedule(JSON.parse(JSON.stringify(savedSchedule)));
if (savedSchedule.mode !== ScheduleMode.DISABLED) {
setActiveTab(savedSchedule.mode);
} else {
setActiveTab(ScheduleMode.CRON);
}
};
const handleSaveScheduleClick = async () => {
if (isLocked) return;
let settingsToSave = { ...localSchedule };
// 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;
}
}
// Determine the effective settings based on the current view (tab) and inputs
const settingsToSave = deriveEffectiveSchedule(localSchedule, activeTab);
// Call API
const success = await onSaveSchedule(settingsToSave);
if (success) {
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">
<button
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
${isScheduleDirty
${isScheduleActionable
? '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'}`}
>
@@ -565,9 +579,9 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
</button>
<button
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
${isScheduleDirty
${isScheduleActionable
? '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'}`}
>
@@ -614,4 +628,4 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
);
};
export default StrategySelector;
export default StrategySelector;