Skip to content

Commit cfcdbaa

Browse files
authored
Merge pull request #86 from beNative/codex/improve-task-builder-pane-design
Reorder task builder tabs and add step collapse controls
2 parents ed07662 + c768d03 commit cfcdbaa

1 file changed

Lines changed: 248 additions & 78 deletions

File tree

components/modals/RepoFormModal.tsx

Lines changed: 248 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -228,12 +228,26 @@ const TaskStepItem: React.FC<{
228228
suggestions: ProjectSuggestion[];
229229
projectInfo: ProjectInfo | null;
230230
delphiVersions: { name: string; version: string }[];
231-
}> = ({ step, index, totalSteps, onStepChange, onMoveStep, onRemoveStep, onDuplicateStep, suggestions, projectInfo, delphiVersions }) => {
231+
collapsed: boolean;
232+
onCollapsedChange: (id: string, collapsed: boolean) => void;
233+
}> = ({
234+
step,
235+
index,
236+
totalSteps,
237+
onStepChange,
238+
onMoveStep,
239+
onRemoveStep,
240+
onDuplicateStep,
241+
suggestions,
242+
projectInfo,
243+
delphiVersions,
244+
collapsed,
245+
onCollapsedChange,
246+
}) => {
232247
const logger = useLogger();
233248
const stepDef = STEP_DEFINITIONS[step.type];
234249
const isEnabled = step.enabled ?? true;
235250
const hasDetails = STEPS_WITH_DETAILS.has(step.type);
236-
const [isCollapsed, setIsCollapsed] = useState(false);
237251
const detailsId = useMemo(() => `task-step-${step.id}-details`, [step.id]);
238252

239253
// --- HOOKS MOVED TO TOP ---
@@ -242,11 +256,14 @@ const TaskStepItem: React.FC<{
242256
const moveToTopTooltip = useTooltip('Move Step to Top');
243257
const moveToBottomTooltip = useTooltip('Move Step to Bottom');
244258

259+
const canCollapse = hasDetails;
260+
const isCollapsed = canCollapse ? collapsed : false;
261+
245262
useEffect(() => {
246-
if (!hasDetails) {
247-
setIsCollapsed(false);
263+
if (!canCollapse && collapsed) {
264+
onCollapsedChange(step.id, false);
248265
}
249-
}, [hasDetails]);
266+
}, [canCollapse, collapsed, onCollapsedChange, step.id]);
250267

251268
const selectedDelphiProject = useMemo(() => {
252269
return projectInfo?.delphi?.projects.find(p => p.path === step.delphiProjectFile);
@@ -546,7 +563,7 @@ const TaskStepItem: React.FC<{
546563
{hasDetails ? (
547564
<button
548565
type="button"
549-
onClick={() => setIsCollapsed(prev => !prev)}
566+
onClick={() => onCollapsedChange(step.id, !isCollapsed)}
550567
aria-label={`${isCollapsed ? 'Expand' : 'Collapse'} step details`}
551568
aria-expanded={!isCollapsed}
552569
aria-controls={detailsId}
@@ -1360,10 +1377,19 @@ const TaskStepsEditor: React.FC<{
13601377
}> = ({ task, setTask, repository, onAddTask }) => {
13611378
const logger = useLogger();
13621379
const [isAddingStep, setIsAddingStep] = useState(false);
1380+
const [activeDetailTab, setActiveDetailTab] = useState<'variables' | 'steps'>('steps');
13631381
const [suggestions, setSuggestions] = useState<ProjectSuggestion[]>([]);
13641382
const [projectInfo, setProjectInfo] = useState<ProjectInfo | null>(null);
13651383
const [delphiVersions, setDelphiVersions] = useState<{ name: string; version: string }[]>([]);
1384+
const [stepCollapseState, setStepCollapseState] = useState<Record<string, boolean>>({});
1385+
const lastTaskIdRef = useRef<string | null>(null);
13661386
const showOnDashboardTooltip = useTooltip('Show this task as a button on the repository card');
1387+
1388+
useEffect(() => {
1389+
if (activeDetailTab !== 'steps' && isAddingStep) {
1390+
setIsAddingStep(false);
1391+
}
1392+
}, [activeDetailTab, isAddingStep]);
13671393

13681394
useEffect(() => {
13691395
if (repository?.localPath) {
@@ -1394,7 +1420,93 @@ const TaskStepsEditor: React.FC<{
13941420
.catch(e => logger.error('Failed to get Delphi versions', e));
13951421
}
13961422
}, [projectInfo, logger]);
1397-
1423+
1424+
useEffect(() => {
1425+
const currentTaskId = task.id ?? '__no_id__';
1426+
if (currentTaskId !== lastTaskIdRef.current) {
1427+
setActiveDetailTab('steps');
1428+
1429+
const initialState: Record<string, boolean> = {};
1430+
task.steps.forEach(step => {
1431+
initialState[step.id] = STEPS_WITH_DETAILS.has(step.type);
1432+
});
1433+
setStepCollapseState(initialState);
1434+
lastTaskIdRef.current = currentTaskId;
1435+
}
1436+
}, [task.id, task.steps]);
1437+
1438+
useEffect(() => {
1439+
setStepCollapseState(prev => {
1440+
const nextState: Record<string, boolean> = {};
1441+
let changed = false;
1442+
task.steps.forEach(step => {
1443+
const supportsCollapse = STEPS_WITH_DETAILS.has(step.type);
1444+
const previousValue = prev[step.id];
1445+
const desiredValue = supportsCollapse ? (previousValue ?? false) : false;
1446+
nextState[step.id] = desiredValue;
1447+
if (previousValue !== desiredValue) {
1448+
changed = true;
1449+
}
1450+
});
1451+
1452+
const sameSize = Object.keys(prev).length === Object.keys(nextState).length;
1453+
const sameEntries = sameSize && Object.keys(nextState).every(key => prev[key] === nextState[key]);
1454+
if (!changed && sameEntries) {
1455+
return prev;
1456+
}
1457+
1458+
return nextState;
1459+
});
1460+
}, [task.steps]);
1461+
1462+
const collapsibleStepCount = useMemo(
1463+
() => task.steps.filter(step => STEPS_WITH_DETAILS.has(step.type)).length,
1464+
[task.steps]
1465+
);
1466+
1467+
const handleCollapsedChange = useCallback((stepId: string, collapsed: boolean) => {
1468+
setStepCollapseState(prev => {
1469+
if (prev[stepId] === collapsed) {
1470+
return prev;
1471+
}
1472+
return { ...prev, [stepId]: collapsed };
1473+
});
1474+
}, []);
1475+
1476+
const handleCollapseAllSteps = useCallback(() => {
1477+
setStepCollapseState(prev => {
1478+
const nextState: Record<string, boolean> = {};
1479+
task.steps.forEach(step => {
1480+
nextState[step.id] = STEPS_WITH_DETAILS.has(step.type);
1481+
});
1482+
1483+
const sameSize = Object.keys(prev).length === Object.keys(nextState).length;
1484+
const sameEntries = sameSize && Object.keys(nextState).every(key => prev[key] === nextState[key]);
1485+
if (sameEntries) {
1486+
return prev;
1487+
}
1488+
1489+
return nextState;
1490+
});
1491+
}, [task.steps]);
1492+
1493+
const handleExpandAllSteps = useCallback(() => {
1494+
setStepCollapseState(prev => {
1495+
const nextState: Record<string, boolean> = {};
1496+
task.steps.forEach(step => {
1497+
nextState[step.id] = false;
1498+
});
1499+
1500+
const sameSize = Object.keys(prev).length === Object.keys(nextState).length;
1501+
const sameEntries = sameSize && Object.keys(nextState).every(key => prev[key] === nextState[key]);
1502+
if (sameEntries) {
1503+
return prev;
1504+
}
1505+
1506+
return nextState;
1507+
});
1508+
}, [task.steps]);
1509+
13981510
const handleAddStep = (type: TaskStepType) => {
13991511
const newStep: TaskStep = { id: `step_${Date.now()}`, type, enabled: true };
14001512
if (type === TaskStepType.RunCommand) newStep.command = suggestions.length > 0 ? suggestions[0].value : 'npm run build';
@@ -1472,12 +1584,19 @@ const TaskStepsEditor: React.FC<{
14721584
});
14731585
}, [repository?.vcs, projectInfo]);
14741586

1587+
const tabButtonClass = (tab: 'variables' | 'steps') =>
1588+
`whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm transition-colors ${
1589+
activeDetailTab === tab
1590+
? 'border-blue-500 text-blue-600'
1591+
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
1592+
}`;
1593+
14751594
return (
14761595
<div className="space-y-3">
14771596
<div className="flex items-center justify-between gap-4">
1478-
<input
1479-
type="text"
1480-
value={task.name}
1597+
<input
1598+
type="text"
1599+
value={task.name}
14811600
onChange={e => setTask({ ...task, name: e.target.value })}
14821601
placeholder="Task Name"
14831602
className="flex-grow text-lg font-bold bg-transparent border-b-2 border-gray-200 dark:border-gray-700 focus:border-blue-500 focus:ring-0 pb-0.5"
@@ -1490,80 +1609,131 @@ const TaskStepsEditor: React.FC<{
14901609
</label>
14911610
</div>
14921611
</div>
1493-
1494-
<div className="space-y-2.5">
1495-
<TaskVariablesEditor variables={task.variables} onVariablesChange={handleVariablesChange} />
1496-
<TaskEnvironmentVariablesEditor variables={task.environmentVariables} onVariablesChange={handleEnvironmentVariablesChange} />
1497-
</div>
1498-
1499-
{task.steps.length === 0 && (
1500-
<div className="text-center py-5 px-4 bg-gray-50 dark:bg-gray-900/50 rounded-lg border-2 border-dashed border-gray-300 dark:border-gray-700">
1501-
<CubeTransparentIcon className="mx-auto h-10 w-10 text-gray-400" />
1502-
<h3 className="mt-2 text-sm font-medium text-gray-800 dark:text-gray-200">This task has no steps.</h3>
1503-
<p className="mt-1 text-xs text-gray-500">Add steps manually to begin.</p>
1504-
</div>
1505-
)}
1506-
1507-
<DockerTaskGenerator dockerCaps={projectInfo?.docker} onAddTask={onAddTask} />
1508-
<NodejsTaskGenerator nodejsCaps={projectInfo?.nodejs} onAddTask={onAddTask} />
1509-
<GoTaskGenerator goCaps={projectInfo?.go} onAddTask={onAddTask} />
1510-
<RustTaskGenerator rustCaps={projectInfo?.rust} onAddTask={onAddTask} />
1511-
<MavenTaskGenerator mavenCaps={projectInfo?.maven} onAddTask={onAddTask} />
1512-
<DotnetTaskGenerator dotnetCaps={projectInfo?.dotnet} onAddTask={onAddTask} />
1513-
<PythonTaskGenerator pythonCaps={projectInfo?.python} onAddTask={onAddTask} />
1514-
<LazarusTaskGenerator lazarusCaps={projectInfo?.lazarus} onAddTask={onAddTask} />
1515-
<DelphiTaskGenerator delphiCaps={projectInfo?.delphi} onAddTask={onAddTask} />
1516-
1517-
<div className="space-y-2">
1518-
{task.steps.map((step, index) => (
1519-
<TaskStepItem
1520-
key={step.id}
1521-
step={step}
1522-
index={index}
1523-
totalSteps={task.steps.length}
1524-
onStepChange={handleStepChange}
1525-
onMoveStep={handleMoveStep}
1526-
onRemoveStep={handleRemoveStep}
1527-
onDuplicateStep={handleDuplicateStep}
1528-
suggestions={suggestions}
1529-
projectInfo={projectInfo}
1530-
delphiVersions={delphiVersions}
1531-
/>
1532-
))}
1533-
</div>
15341612

1535-
{isAddingStep && (
1536-
<div className="space-y-3">
1537-
{STEP_CATEGORIES.map(category => {
1538-
const relevantSteps = category.types.filter(type => availableSteps.includes(type));
1539-
if (relevantSteps.length === 0) return null;
1613+
<div>
1614+
<div className="flex items-end justify-between border-b border-gray-200 dark:border-gray-700">
1615+
<nav className="-mb-px flex space-x-4">
1616+
<button type="button" onClick={() => setActiveDetailTab('steps')} className={tabButtonClass('steps')}>
1617+
Steps
1618+
</button>
1619+
<button type="button" onClick={() => setActiveDetailTab('variables')} className={tabButtonClass('variables')}>
1620+
Variables
1621+
</button>
1622+
</nav>
15401623

1541-
return (
1542-
<div key={category.name}>
1624+
{activeDetailTab === 'steps' && task.steps.length > 0 && (
1625+
<div className="flex items-center gap-2 text-xs pb-2">
1626+
<button
1627+
type="button"
1628+
onClick={handleCollapseAllSteps}
1629+
disabled={collapsibleStepCount === 0}
1630+
className="font-medium text-gray-600 dark:text-gray-300 hover:text-blue-600 disabled:text-gray-400 disabled:cursor-not-allowed"
1631+
>
1632+
Collapse all
1633+
</button>
1634+
<span className="text-gray-300 dark:text-gray-600" aria-hidden="true">|</span>
1635+
<button
1636+
type="button"
1637+
onClick={handleExpandAllSteps}
1638+
className="font-medium text-gray-600 dark:text-gray-300 hover:text-blue-600"
1639+
>
1640+
Expand all
1641+
</button>
1642+
</div>
1643+
)}
1644+
</div>
1645+
1646+
<div className="mt-4 space-y-4">
1647+
{activeDetailTab === 'variables' ? (
1648+
<div className="space-y-2.5">
1649+
<TaskVariablesEditor variables={task.variables} onVariablesChange={handleVariablesChange} />
1650+
<TaskEnvironmentVariablesEditor variables={task.environmentVariables} onVariablesChange={handleEnvironmentVariablesChange} />
1651+
</div>
1652+
) : (
1653+
<>
1654+
{task.steps.length === 0 && (
1655+
<div className="text-center py-5 px-4 bg-gray-50 dark:bg-gray-900/50 rounded-lg border-2 border-dashed border-gray-300 dark:border-gray-700">
1656+
<CubeTransparentIcon className="mx-auto h-10 w-10 text-gray-400" />
1657+
<h3 className="mt-2 text-sm font-medium text-gray-800 dark:text-gray-200">This task has no steps.</h3>
1658+
<p className="mt-1 text-xs text-gray-500">Add steps manually to begin.</p>
1659+
</div>
1660+
)}
1661+
1662+
<DockerTaskGenerator dockerCaps={projectInfo?.docker} onAddTask={onAddTask} />
1663+
<NodejsTaskGenerator nodejsCaps={projectInfo?.nodejs} onAddTask={onAddTask} />
1664+
<GoTaskGenerator goCaps={projectInfo?.go} onAddTask={onAddTask} />
1665+
<RustTaskGenerator rustCaps={projectInfo?.rust} onAddTask={onAddTask} />
1666+
<MavenTaskGenerator mavenCaps={projectInfo?.maven} onAddTask={onAddTask} />
1667+
<DotnetTaskGenerator dotnetCaps={projectInfo?.dotnet} onAddTask={onAddTask} />
1668+
<PythonTaskGenerator pythonCaps={projectInfo?.python} onAddTask={onAddTask} />
1669+
<LazarusTaskGenerator lazarusCaps={projectInfo?.lazarus} onAddTask={onAddTask} />
1670+
<DelphiTaskGenerator delphiCaps={projectInfo?.delphi} onAddTask={onAddTask} />
1671+
1672+
<div className="space-y-2">
1673+
{task.steps.map((step, index) => (
1674+
<TaskStepItem
1675+
key={step.id}
1676+
step={step}
1677+
index={index}
1678+
totalSteps={task.steps.length}
1679+
onStepChange={handleStepChange}
1680+
onMoveStep={handleMoveStep}
1681+
onRemoveStep={handleRemoveStep}
1682+
onDuplicateStep={handleDuplicateStep}
1683+
suggestions={suggestions}
1684+
projectInfo={projectInfo}
1685+
delphiVersions={delphiVersions}
1686+
collapsed={!!stepCollapseState[step.id]}
1687+
onCollapsedChange={handleCollapsedChange}
1688+
/>
1689+
))}
1690+
</div>
1691+
1692+
{isAddingStep && (
1693+
<div className="space-y-3">
1694+
{STEP_CATEGORIES.map(category => {
1695+
const relevantSteps = category.types.filter(type => availableSteps.includes(type));
1696+
if (relevantSteps.length === 0) return null;
1697+
1698+
return (
1699+
<div key={category.name}>
15431700
<h4 className="text-sm font-semibold text-gray-600 dark:text-gray-400 mb-2">{category.name}</h4>
15441701
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2.5">
1545-
{relevantSteps.map(type => {
1546-
const { label, icon: Icon, description } = STEP_DEFINITIONS[type];
1547-
return (
1548-
<button key={type} type="button" onClick={() => handleAddStep(type)} className="text-left p-2 bg-gray-100 dark:bg-gray-900/50 rounded-lg hover:bg-blue-500/10 hover:ring-2 ring-blue-500 transition-all">
1549-
<div className="flex items-center gap-3">
1550-
<Icon className="h-6 w-6 text-blue-500" />
1551-
<p className="font-semibold text-gray-800 dark:text-gray-200">{label}</p>
1552-
</div>
1553-
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">{description}</p>
1554-
</button>
1555-
);
1556-
})}
1702+
{relevantSteps.map(type => {
1703+
const { label, icon: Icon, description } = STEP_DEFINITIONS[type];
1704+
return (
1705+
<button
1706+
key={type}
1707+
type="button"
1708+
onClick={() => handleAddStep(type)}
1709+
className="text-left p-2 bg-gray-100 dark:bg-gray-900/50 rounded-lg hover:bg-blue-500/10 hover:ring-2 ring-blue-500 transition-all"
1710+
>
1711+
<div className="flex items-center gap-3">
1712+
<Icon className="h-6 w-6 text-blue-500" />
1713+
<p className="font-semibold text-gray-800 dark:text-gray-200">{label}</p>
1714+
</div>
1715+
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">{description}</p>
1716+
</button>
1717+
);
1718+
})}
15571719
</div>
1558-
</div>
1559-
);
1560-
})}
1720+
</div>
1721+
);
1722+
})}
1723+
</div>
1724+
)}
1725+
1726+
<button
1727+
type="button"
1728+
onClick={() => setIsAddingStep(p => !p)}
1729+
className="flex items-center text-sm font-medium text-blue-600 dark:text-blue-400 hover:underline"
1730+
>
1731+
<PlusIcon className="h-4 w-4 mr-1" /> {isAddingStep ? 'Cancel' : 'Add Step'}
1732+
</button>
1733+
</>
1734+
)}
15611735
</div>
1562-
)}
1563-
1564-
<button type="button" onClick={() => setIsAddingStep(p => !p)} className="flex items-center text-sm font-medium text-blue-600 dark:text-blue-400 hover:underline">
1565-
<PlusIcon className="h-4 w-4 mr-1"/> {isAddingStep ? 'Cancel' : 'Add Step'}
1566-
</button>
1736+
</div>
15671737
</div>
15681738
);
15691739
};

0 commit comments

Comments
 (0)