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:
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user