3 Commits

Author SHA1 Message Date
Koha9 c982fb930f Merge commit '6f234ebc48e506f0c46ebf811b2a791dd8960dcd' into scheduling-function 2025-11-29 10:54:08 +09:00
Koha9 6f234ebc48 PlexPlaylist_UI subtree merge
feat(ui): Improve schedule dirty state detection
Detects changes in schedule settings more accurately, considering the active tab and deriving the effective schedule state before comparison. This prevents unintended saving of disabled schedules when switching tabs.

Merge commit '305743d752e1a1ecaefba79419929524ad060663'
2025-11-29 10:52:05 +09:00
Koha9 305743d752 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
2025-11-29 10:52:05 +09:00
@@ -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;