Skip to content

Commit 3c3cbae

Browse files
committed
Add collapsible details to task step editor
1 parent 1762764 commit 3c3cbae

File tree

1 file changed

+91
-43
lines changed

1 file changed

+91
-43
lines changed

components/modals/RepoFormModal.tsx

Lines changed: 91 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ import { TagIcon } from '../icons/TagIcon';
3232
import { CubeIcon } from '../icons/CubeIcon';
3333
import { ChevronsUpIcon } from '../icons/ChevronsUpIcon';
3434
import { ChevronsDownIcon } from '../icons/ChevronsDownIcon';
35+
import { ChevronDownIcon } from '../icons/ChevronDownIcon';
36+
import { ChevronRightIcon } from '../icons/ChevronRightIcon';
3537
import ReactMarkdown from 'react-markdown';
3638
import remarkGfm from 'remark-gfm';
3739
import { PencilIcon } from '../icons/PencilIcon';
@@ -131,6 +133,19 @@ const STEP_DEFINITIONS: Record<TaskStepType, { label: string; icon: React.Compon
131133
[TaskStepType.DOCKER_COMPOSE_BUILD]: { label: 'Docker: Compose Build', icon: DockerIcon, description: 'Build or rebuild services with Docker Compose.' },
132134
};
133135

136+
const STEPS_WITH_DETAILS = new Set<TaskStepType>([
137+
TaskStepType.GitCheckout,
138+
TaskStepType.SvnSwitch,
139+
TaskStepType.DelphiBuild,
140+
TaskStepType.LAZARUS_BUILD,
141+
TaskStepType.LAZARUS_BUILD_PACKAGE,
142+
TaskStepType.FPC_TEST_FPCUNIT,
143+
TaskStepType.DELPHI_PACKAGE_INNO,
144+
TaskStepType.DELPHI_PACKAGE_NSIS,
145+
TaskStepType.DELPHI_TEST_DUNITX,
146+
TaskStepType.RunCommand,
147+
]);
148+
134149
const STEP_CATEGORIES = [
135150
{ name: 'General', types: [TaskStepType.RunCommand] },
136151
{ name: 'Git', types: [TaskStepType.GitPull, TaskStepType.GitFetch, TaskStepType.GitCheckout, TaskStepType.GitStash] },
@@ -217,12 +232,21 @@ const TaskStepItem: React.FC<{
217232
const logger = useLogger();
218233
const stepDef = STEP_DEFINITIONS[step.type];
219234
const isEnabled = step.enabled ?? true;
235+
const hasDetails = STEPS_WITH_DETAILS.has(step.type);
236+
const [isCollapsed, setIsCollapsed] = useState(false);
237+
const detailsId = useMemo(() => `task-step-${step.id}-details`, [step.id]);
220238

221239
// --- HOOKS MOVED TO TOP ---
222240
const toggleTooltip = useTooltip(isEnabled ? 'Disable Step' : 'Enable Step');
223241
const duplicateTooltip = useTooltip('Duplicate Step');
224242
const moveToTopTooltip = useTooltip('Move Step to Top');
225243
const moveToBottomTooltip = useTooltip('Move Step to Bottom');
244+
245+
useEffect(() => {
246+
if (!hasDetails) {
247+
setIsCollapsed(false);
248+
}
249+
}, [hasDetails]);
226250

227251
const selectedDelphiProject = useMemo(() => {
228252
return projectInfo?.delphi?.projects.find(p => p.path === step.delphiProjectFile);
@@ -301,47 +325,8 @@ const TaskStepItem: React.FC<{
301325
</div>
302326
);
303327

304-
return (
305-
<div className={`bg-white dark:bg-gray-800/50 p-3 rounded-lg border border-gray-200 dark:border-gray-700 space-y-2 transition-opacity ${!isEnabled ? 'opacity-50' : ''}`}>
306-
<div className="flex items-center justify-between">
307-
<div className="flex items-center gap-3">
308-
<Icon className="h-6 w-6 text-blue-500" />
309-
<div>
310-
<p className="font-semibold text-gray-800 dark:text-gray-200">{label}</p>
311-
<p className="text-xs text-gray-500">Step {index + 1}</p>
312-
</div>
313-
</div>
314-
<div className="flex items-center space-x-2">
315-
<label {...toggleTooltip} className="relative inline-flex items-center cursor-pointer">
316-
<input type="checkbox" checked={isEnabled} onChange={(e) => onStepChange(step.id, {enabled: e.target.checked})} className="sr-only peer" />
317-
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-blue-500/50 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
318-
</label>
319-
<button
320-
{...moveToTopTooltip}
321-
type="button"
322-
onClick={() => onMoveStep(index, 'top')}
323-
disabled={index === 0}
324-
aria-label="Move step to top"
325-
className="p-1.5 disabled:opacity-30 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-full"
326-
>
327-
<ChevronsUpIcon className="h-4 w-4" />
328-
</button>
329-
<button type="button" onClick={() => onMoveStep(index, 'up')} disabled={index === 0} className="p-1.5 disabled:opacity-30 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-full"><ArrowUpIcon className="h-4 w-4" /></button>
330-
<button type="button" onClick={() => onMoveStep(index, 'down')} disabled={index === totalSteps - 1} className="p-1.5 disabled:opacity-30 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-full"><ArrowDownIcon className="h-4 w-4" /></button>
331-
<button
332-
{...moveToBottomTooltip}
333-
type="button"
334-
onClick={() => onMoveStep(index, 'bottom')}
335-
disabled={index === totalSteps - 1}
336-
aria-label="Move step to bottom"
337-
className="p-1.5 disabled:opacity-30 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-full"
338-
>
339-
<ChevronsDownIcon className="h-4 w-4" />
340-
</button>
341-
<button {...duplicateTooltip} type="button" onClick={() => onDuplicateStep(index)} className="p-1.5 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-full"><DocumentDuplicateIcon className="h-4 w-4" /></button>
342-
<button type="button" onClick={() => onRemoveStep(step.id)} className="p-1.5 text-red-500 hover:bg-red-100 dark:hover:bg-red-900/50 rounded-full"><TrashIcon className="h-4 w-4" /></button>
343-
</div>
344-
</div>
328+
const detailFields = (
329+
<>
345330
{(step.type === TaskStepType.GitCheckout || step.type === TaskStepType.SvnSwitch) && (
346331
<div>
347332
<label className="text-xs font-medium text-gray-500 dark:text-gray-400">
@@ -517,7 +502,7 @@ const TaskStepItem: React.FC<{
517502
(acc[suggestion.group] = acc[suggestion.group] || []).push(suggestion);
518503
return acc;
519504
}, {} as Record<string, ProjectSuggestion[]>);
520-
505+
521506
const handleSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
522507
const newValue = e.target.value;
523508
// When a predefined command is selected, update the step.
@@ -538,7 +523,7 @@ const TaskStepItem: React.FC<{
538523
))}
539524
<option value={CUSTOM_COMMAND_VALUE}>Custom Command...</option>
540525
</select>
541-
526+
542527
<textarea
543528
placeholder={`e.g., npm run build -- --env=production\nUse \${VAR_NAME} for variables.`}
544529
value={step.command || ''}
@@ -550,6 +535,69 @@ const TaskStepItem: React.FC<{
550535
</div>
551536
);
552537
})()}
538+
</>
539+
);
540+
541+
return (
542+
<div className={`bg-white dark:bg-gray-800/50 p-3 rounded-lg border border-gray-200 dark:border-gray-700 space-y-2 transition-opacity ${!isEnabled ? 'opacity-50' : ''}`}>
543+
<div className="flex items-center justify-between">
544+
<div className="flex items-center gap-3">
545+
{hasDetails && (
546+
<button
547+
type="button"
548+
onClick={() => setIsCollapsed(prev => !prev)}
549+
aria-label={`${isCollapsed ? 'Expand' : 'Collapse'} step details`}
550+
aria-expanded={!isCollapsed}
551+
aria-controls={detailsId}
552+
className="p-1.5 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-full"
553+
>
554+
{isCollapsed ? <ChevronRightIcon className="h-4 w-4" /> : <ChevronDownIcon className="h-4 w-4" />}
555+
</button>
556+
)}
557+
<Icon className="h-6 w-6 text-blue-500" />
558+
<div>
559+
<p className="font-semibold text-gray-800 dark:text-gray-200">{label}</p>
560+
<p className="text-xs text-gray-500">Step {index + 1}</p>
561+
</div>
562+
</div>
563+
<div className="flex items-center space-x-2">
564+
<label {...toggleTooltip} className="relative inline-flex items-center cursor-pointer">
565+
<input type="checkbox" checked={isEnabled} onChange={(e) => onStepChange(step.id, {enabled: e.target.checked})} className="sr-only peer" />
566+
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-blue-500/50 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
567+
</label>
568+
<button
569+
{...moveToTopTooltip}
570+
type="button"
571+
onClick={() => onMoveStep(index, 'top')}
572+
disabled={index === 0}
573+
aria-label="Move step to top"
574+
className="p-1.5 disabled:opacity-30 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-full"
575+
>
576+
<ChevronsUpIcon className="h-4 w-4" />
577+
</button>
578+
<button type="button" onClick={() => onMoveStep(index, 'up')} disabled={index === 0} className="p-1.5 disabled:opacity-30 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-full"><ArrowUpIcon className="h-4 w-4" /></button>
579+
<button type="button" onClick={() => onMoveStep(index, 'down')} disabled={index === totalSteps - 1} className="p-1.5 disabled:opacity-30 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-full"><ArrowDownIcon className="h-4 w-4" /></button>
580+
<button
581+
{...moveToBottomTooltip}
582+
type="button"
583+
onClick={() => onMoveStep(index, 'bottom')}
584+
disabled={index === totalSteps - 1}
585+
aria-label="Move step to bottom"
586+
className="p-1.5 disabled:opacity-30 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-full"
587+
>
588+
<ChevronsDownIcon className="h-4 w-4" />
589+
</button>
590+
<button {...duplicateTooltip} type="button" onClick={() => onDuplicateStep(index)} className="p-1.5 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-full"><DocumentDuplicateIcon className="h-4 w-4" /></button>
591+
<button type="button" onClick={() => onRemoveStep(step.id)} className="p-1.5 text-red-500 hover:bg-red-100 dark:hover:bg-red-900/50 rounded-full"><TrashIcon className="h-4 w-4" /></button>
592+
</div>
593+
</div>
594+
{hasDetails ? (
595+
<div id={detailsId} className={`mt-3 space-y-3 ${isCollapsed ? 'hidden' : ''}`}>
596+
{detailFields}
597+
</div>
598+
) : (
599+
detailFields
600+
)}
553601
</div>
554602
);
555603
};

0 commit comments

Comments
 (0)