feat(03-05): implement DeploymentStep and StepIndicator

- DeploymentStep: includeInstall toggle, configPath radio, scriptTargets checkboxes
- DeploymentStep: dispatches SET_DEPLOYMENT live on every control change
- DeploymentStep: Back button (SET_STEP(1)), Next/Review button (SET_STEP(3))
- StepIndicator: 3-step breadcrumb with checkmarks on completed steps
- StepIndicator: clicking step 0 dispatches SET_REMOTE_PARAMS({}) then SET_STEP(0)
- StepIndicator: never dispatches RESET — deployment options preserved
- All 5 WIZD-03 tests GREEN
This commit is contained in:
2026-03-27 09:43:50 +01:00
parent 7ffccfb7f8
commit 273789f704
2 changed files with 160 additions and 0 deletions
+106
View File
@@ -0,0 +1,106 @@
// src/components/wizard/DeploymentStep.tsx
// Step 2 — deployment options: includeInstall toggle, configPath radio, scriptTargets checkboxes.
// Dispatches SET_DEPLOYMENT live on every control change (no submit needed for deployment options).
import { useWizard } from '../../store/context';
export function DeploymentStep() {
const { state, dispatch } = useWizard();
const { includeInstall, configPath, scriptTargets } = state.deployment;
function handleScriptTargetChange(target: 'intune' | 'rmm', checked: boolean) {
let next: ('intune' | 'rmm')[];
if (checked) {
next = scriptTargets.includes(target) ? scriptTargets : [...scriptTargets, target];
} else {
next = scriptTargets.filter((t) => t !== target);
}
dispatch({ type: 'SET_DEPLOYMENT', payload: { scriptTargets: next } });
}
return (
<div>
<h2>Step 3: Deployment Options</h2>
{/* Include rclone installation toggle */}
<div>
<label>
<input
type="checkbox"
checked={includeInstall}
onChange={(e) =>
dispatch({ type: 'SET_DEPLOYMENT', payload: { includeInstall: e.target.checked } })
}
/>
{' '}Include rclone installation
</label>
</div>
{/* Config deployment path radio group */}
<fieldset>
<legend>Config deployment path</legend>
<label>
<input
type="radio"
name="configPath"
value="machine-wide"
checked={configPath === 'machine-wide'}
onChange={() =>
dispatch({ type: 'SET_DEPLOYMENT', payload: { configPath: 'machine-wide' } })
}
/>
{' '}Machine-wide (C:\ProgramData\rclone\)
</label>
<label>
<input
type="radio"
name="configPath"
value="user-profile"
checked={configPath === 'user-profile'}
onChange={() =>
dispatch({ type: 'SET_DEPLOYMENT', payload: { configPath: 'user-profile' } })
}
/>
{' '}User profile (%APPDATA%\rclone\)
</label>
</fieldset>
{/* Script targets checkboxes */}
<fieldset>
<legend>Script targets</legend>
<label>
<input
type="checkbox"
checked={scriptTargets.includes('intune')}
onChange={(e) => handleScriptTargetChange('intune', e.target.checked)}
/>
{' '}Intune
</label>
<label>
<input
type="checkbox"
checked={scriptTargets.includes('rmm')}
onChange={(e) => handleScriptTargetChange('rmm', e.target.checked)}
/>
{' '}RMM
</label>
</fieldset>
{/* Navigation buttons */}
<div>
<button
type="button"
onClick={() => dispatch({ type: 'SET_STEP', payload: 1 })}
>
Back
</button>
<button
type="button"
onClick={() => dispatch({ type: 'SET_STEP', payload: 3 })}
>
Next / Review
</button>
</div>
</div>
);
}
+54
View File
@@ -0,0 +1,54 @@
// src/components/wizard/StepIndicator.tsx
// Breadcrumb navigation — 1.Backend > 2.Remote Config > 3.Deployment
// Completed steps are clickable. Clicking step 0 also clears remote.params (SET_REMOTE_PARAMS({})).
// CRITICAL: never dispatches RESET — deployment options must be preserved on back navigation.
import { useWizard } from '../../store/context';
const STEP_LABELS = ['Backend', 'Remote Config', 'Deployment'];
export function StepIndicator() {
const { state, dispatch } = useWizard();
const { currentStep } = state;
function handleStepClick(targetStep: number) {
if (targetStep === 0 && currentStep > 0) {
// Clear remote params so the next backend selection starts fresh
// Do NOT dispatch RESET — deployment options must be preserved
dispatch({ type: 'SET_REMOTE_PARAMS', payload: {} });
}
dispatch({ type: 'SET_STEP', payload: targetStep });
}
return (
<nav aria-label="Wizard steps">
{STEP_LABELS.map((label, i) => {
const isCompleted = i < currentStep;
const isActive = i === currentStep;
return (
<span key={i}>
{i > 0 && <span aria-hidden="true"> </span>}
{isCompleted ? (
<button
type="button"
onClick={() => handleStepClick(i)}
style={{ fontWeight: 'normal' }}
>
{i + 1}. {label}
</button>
) : isActive ? (
<span style={{ fontWeight: 'bold' }}>
{i + 1}. {label}
</span>
) : (
<span style={{ color: '#999' }}>
{i + 1}. {label}
</span>
)}
</span>
);
})}
</nav>
);
}