diff --git a/apps/client/lib/src/app/oto_client_app.dart b/apps/client/lib/src/app/oto_client_app.dart index bcc8661..061c2fe 100644 --- a/apps/client/lib/src/app/oto_client_app.dart +++ b/apps/client/lib/src/app/oto_client_app.dart @@ -736,6 +736,7 @@ class _OtoClientAppState extends State { themeAdapter: themeAdapter, jobCreateState: _jobCreateState, showCreateForm: _showJobCreateForm, + createOnly: _showJobCreateForm, onCreateJob: _submitJobCreate, onExecutionTap: (execID) { _loadExecutionDetail(execID); diff --git a/apps/client/test/widget_test.dart b/apps/client/test/widget_test.dart index fc52f93..dda4b87 100644 --- a/apps/client/test/widget_test.dart +++ b/apps/client/test/widget_test.dart @@ -1441,6 +1441,8 @@ void _registerJobCreateTests() { await tester.tap(_newItemMenuButton()); await tester.pumpAndSettle(); expect(find.text('New Item section'), findsOneWidget); + expect(find.text('Pipelines / Jobs'), findsNothing); + expect(find.text('No pipeline jobs'), findsNothing); expect(find.byType(TextField), findsNWidgets(2)); expect(find.text('Create'), findsOneWidget); @@ -1489,7 +1491,6 @@ void _registerJobCreateTests() { ), findsOneWidget, ); - expect(find.textContaining('created-job-1'), findsOneWidget); // Verify refresh fetched the created job await tester.tap(find.byTooltip('Executions')); @@ -1497,6 +1498,7 @@ void _registerJobCreateTests() { await tester.tap(find.byTooltip('Pipelines')); await tester.pumpAndSettle(); expect(find.text('Created Job'), findsOneWidget); + expect(find.text('Job ID: created-job-1'), findsOneWidget); // --- Test failed result shows error message --- createCallCount = 0; @@ -1613,6 +1615,7 @@ void _registerJobCreateTests() { await tester.pumpAndSettle(); expect(find.text('New Item section'), findsOneWidget); + expect(find.text('Pipelines / Jobs'), findsNothing); expect(find.byType(TextField), findsNWidgets(2)); }); @@ -1671,6 +1674,7 @@ void _registerJobCreateTests() { await tester.pumpAndSettle(); expect(find.text('New Item section'), findsOneWidget); + expect(find.text('Pipelines / Jobs'), findsNothing); expect(find.byType(TextField), findsNWidgets(2)); }); } diff --git a/packages/flutter/oto_console/lib/src/oto_jobs_surface.dart b/packages/flutter/oto_console/lib/src/oto_jobs_surface.dart index e4d698f..7422726 100644 --- a/packages/flutter/oto_console/lib/src/oto_jobs_surface.dart +++ b/packages/flutter/oto_console/lib/src/oto_jobs_surface.dart @@ -7,6 +7,7 @@ class OtoJobsSurface extends StatefulWidget { final ValueChanged? onExecutionTap; final OtoActionViewState? jobCreateState; final bool showCreateForm; + final bool createOnly; final ValueChanged? onCreateJob; const OtoJobsSurface({ @@ -16,6 +17,7 @@ class OtoJobsSurface extends StatefulWidget { this.onExecutionTap, this.jobCreateState, this.showCreateForm = true, + this.createOnly = false, this.onCreateJob, }); @@ -48,6 +50,12 @@ class _OtoJobsSurfaceState extends State { Widget build(BuildContext context) { final theme = widget.themeAdapter ?? const OtoConsoleThemeAdapter(); final jobCount = widget.snapshot.data?.length ?? 0; + final showCreateForm = + widget.showCreateForm && widget.jobCreateState != null; + final title = widget.createOnly ? 'New Item' : 'Pipelines / Jobs'; + final icon = widget.createOnly + ? Icons.add_circle_outline + : Icons.account_tree_outlined; return Container( color: theme.backgroundColor, padding: const EdgeInsets.all(24), @@ -56,15 +64,11 @@ class _OtoJobsSurfaceState extends State { children: [ Row( children: [ - Icon( - Icons.account_tree_outlined, - color: theme.primaryColor, - size: 24, - ), + Icon(icon, color: theme.primaryColor, size: 24), const SizedBox(width: 12), Expanded( child: Text( - 'Pipelines / Jobs', + title, style: TextStyle( color: theme.textColor, fontSize: 24, @@ -75,16 +79,21 @@ class _OtoJobsSurfaceState extends State { ], ), const SizedBox(height: 16), - _BuildContextStrip(theme: theme, jobCount: jobCount), - const SizedBox(height: 20), - if (widget.showCreateForm && widget.jobCreateState != null) ...[ + if (!widget.createOnly) ...[ + _BuildContextStrip(theme: theme, jobCount: jobCount), + const SizedBox(height: 20), + ], + if (showCreateForm) ...[ _CreatePanel( theme: theme, child: _buildJobCreateForm(context, theme), ), const SizedBox(height: 20), ], - Expanded(child: _buildContent(context, theme)), + if (!widget.createOnly) + Expanded(child: _buildContent(context, theme)) + else if (!showCreateForm) + Expanded(child: _buildCreateUnavailableState(theme)), ], ), ); @@ -309,6 +318,43 @@ class _OtoJobsSurfaceState extends State { ], ); } + + Widget _buildCreateUnavailableState(OtoConsoleThemeAdapter theme) { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: theme.railBackgroundColor, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: theme.borderColor), + ), + child: Row( + children: [ + Icon(Icons.add_circle_outline, color: theme.primaryColor, size: 32), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Pipeline creation unavailable', + style: TextStyle( + color: theme.textColor, + fontSize: 18, + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(height: 4), + Text( + 'No create surface is mounted.', + style: TextStyle(color: theme.textSecondaryColor), + ), + ], + ), + ), + ], + ), + ); + } } /// Build Queue / Build History context strip shown above the job table. diff --git a/packages/flutter/oto_console/test/oto_console_test.dart b/packages/flutter/oto_console/test/oto_console_test.dart index 47c299b..dcfcbd6 100644 --- a/packages/flutter/oto_console/test/oto_console_test.dart +++ b/packages/flutter/oto_console/test/oto_console_test.dart @@ -103,24 +103,37 @@ void main() { ); final navigations = []; var newItemTaps = 0; + var showCreate = false; await tester.pumpWidget( - MaterialApp( - home: OtoConsoleShell( - config: config, - overview: const OtoConsoleOverview( + StatefulBuilder( + builder: (context, setHostState) => MaterialApp( + home: OtoConsoleShell( config: config, - connection: onlineConnection, + overview: const OtoConsoleOverview( + config: config, + connection: onlineConnection, + ), + pipelines: OtoJobsSurface( + snapshot: const OtoSurfaceSnapshot.empty(), + jobCreateState: const OtoActionViewState.idle(), + showCreateForm: showCreate, + createOnly: showCreate, + onCreateJob: (_) {}, + ), + onNavigate: (section) { + navigations.add(section); + setHostState(() { + showCreate = false; + }); + }, + onNewItem: () { + newItemTaps++; + setHostState(() { + showCreate = true; + }); + }, ), - pipelines: OtoJobsSurface( - snapshot: const OtoSurfaceSnapshot.empty(), - jobCreateState: const OtoActionViewState.idle(), - onCreateJob: (_) {}, - ), - onNavigate: navigations.add, - onNewItem: () { - newItemTaps++; - }, ), ), ); @@ -131,12 +144,17 @@ void main() { expect(navigations, [OtoConsoleSection.pipelines]); expect(newItemTaps, 1); expect(find.text('New Item section'), findsOneWidget); + expect(find.text('Pipelines / Jobs'), findsNothing); + expect(find.text('No pipeline jobs'), findsNothing); expect(find.byType(TextField), findsNWidgets(2)); await tester.tap(find.byTooltip('Pipelines')); await tester.pumpAndSettle(); expect(find.text('Pipelines section'), findsOneWidget); + expect(find.text('Pipelines / Jobs'), findsOneWidget); + expect(find.text('No pipeline jobs'), findsOneWidget); + expect(find.byType(TextField), findsNothing); }); testWidgets('renders section surfaces with empty states', (tester) async { @@ -954,4 +972,28 @@ void main() { expect(find.text('No pipeline jobs'), findsOneWidget); }, ); + + testWidgets('OtoJobsSurface renders a create-only New Item surface', ( + tester, + ) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: OtoJobsSurface( + snapshot: const OtoSurfaceSnapshot.empty(), + jobCreateState: const OtoActionViewState.idle(), + showCreateForm: true, + createOnly: true, + onCreateJob: (draft) {}, + ), + ), + ), + ); + + expect(find.text('Pipelines / Jobs'), findsNothing); + expect(find.text('Build Queue'), findsNothing); + expect(find.text('No pipeline jobs'), findsNothing); + expect(find.byType(TextField), findsNWidgets(2)); + expect(find.text('Create'), findsOneWidget); + }); }