@@ -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