From 3c6c2aa50add5e7ab92f3fb5530d1b4541f4bb55 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 7 Oct 2021 12:48:04 -0400 Subject: [PATCH] Alex chen conductor ui3 (#91118) --- .../ui/lib/widgets/conductor_status.dart | 33 ++++++ dev/conductor/ui/lib/widgets/progression.dart | 108 +++++++++++++++++- dev/conductor/ui/lib/widgets/substeps.dart | 89 +++++++++++++++ .../ui/test/widgets/stepper_test.dart | 86 ++++++++++++++ 4 files changed, 311 insertions(+), 5 deletions(-) create mode 100644 dev/conductor/ui/lib/widgets/conductor_status.dart create mode 100644 dev/conductor/ui/lib/widgets/substeps.dart create mode 100644 dev/conductor/ui/test/widgets/stepper_test.dart diff --git a/dev/conductor/ui/lib/widgets/conductor_status.dart b/dev/conductor/ui/lib/widgets/conductor_status.dart new file mode 100644 index 0000000000..46614553b9 --- /dev/null +++ b/dev/conductor/ui/lib/widgets/conductor_status.dart @@ -0,0 +1,33 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:conductor_core/conductor_core.dart'; +import 'package:conductor_core/proto.dart' as pb; +import 'package:flutter/material.dart'; + +/// Display the current conductor state +class ConductorStatus extends StatefulWidget { + const ConductorStatus({ + Key? key, + this.releaseState, + required this.stateFilePath, + }) : super(key: key); + + final pb.ConductorState? releaseState; + final String stateFilePath; + + @override + ConductorStatusState createState() => ConductorStatusState(); +} + +class ConductorStatusState extends State { + @override + Widget build(BuildContext context) { + return SelectableText( + widget.releaseState != null + ? presentState(widget.releaseState!) + : 'No persistent state file found at ${widget.stateFilePath}', + ); + } +} diff --git a/dev/conductor/ui/lib/widgets/progression.dart b/dev/conductor/ui/lib/widgets/progression.dart index 4020f4124c..0bb97e917d 100644 --- a/dev/conductor/ui/lib/widgets/progression.dart +++ b/dev/conductor/ui/lib/widgets/progression.dart @@ -2,10 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:conductor_core/conductor_core.dart'; import 'package:conductor_core/proto.dart' as pb; import 'package:flutter/material.dart'; +import 'conductor_status.dart'; +import 'substeps.dart'; + /// Displays the progression and each step of the release from the conductor. /// // TODO(Yugue): Add documentation to explain @@ -22,20 +24,116 @@ class MainProgression extends StatefulWidget { @override MainProgressionState createState() => MainProgressionState(); + + static const List _stepTitles = [ + 'Initialize a New Flutter Release', + 'Flutter Engine Cherrypicks', + 'Flutter Framework Cherrypicks', + 'Publish the Release', + 'Release is Successfully published' + ]; } class MainProgressionState extends State { + int _completedStep = 0; + + // Move forward the stepper to the next step of the release. + void nextStep() { + if (_completedStep < MainProgression._stepTitles.length - 1) { + setState(() { + _completedStep += 1; + }); + } + } + + /// Change each step's state according to [_completedStep]. + StepState handleStepState(int index) { + if (_completedStep > index) { + return StepState.complete; + } else if (_completedStep == index) { + return StepState.indexed; + } else { + return StepState.disabled; + } + } + + final ScrollController _scrollController = ScrollController(); + @override Widget build(BuildContext context) { return Expanded( child: Scrollbar( isAlwaysShown: true, + controller: _scrollController, child: ListView( + controller: _scrollController, + padding: const EdgeInsets.symmetric(vertical: 12.0), + physics: const ClampingScrollPhysics(), children: [ - SelectableText( - widget.releaseState != null - ? presentState(widget.releaseState!) - : 'No persistent state file found at ${widget.stateFilePath}', + ConductorStatus( + releaseState: widget.releaseState, + stateFilePath: widget.stateFilePath, + ), + Stepper( + controlsBuilder: (BuildContext context, ControlsDetails details) { + return Row( + children: const [], + ); + }, + type: StepperType.vertical, + physics: const ScrollPhysics(), + currentStep: _completedStep, + onStepContinue: nextStep, + steps: [ + Step( + title: Text(MainProgression._stepTitles[0]), + content: Column( + children: [ + ConductorSubsteps(nextStep: nextStep), + ], + ), + isActive: true, + state: handleStepState(0), + ), + Step( + title: Text(MainProgression._stepTitles[1]), + content: Column( + children: [ + ConductorSubsteps(nextStep: nextStep), + ], + ), + isActive: true, + state: handleStepState(1), + ), + Step( + title: Text(MainProgression._stepTitles[2]), + content: Column( + children: [ + ConductorSubsteps(nextStep: nextStep), + ], + ), + isActive: true, + state: handleStepState(2), + ), + Step( + title: Text(MainProgression._stepTitles[3]), + content: Column( + children: [ + ConductorSubsteps(nextStep: nextStep), + ], + ), + isActive: true, + state: handleStepState(3), + ), + Step( + title: Text(MainProgression._stepTitles[4]), + content: Column( + children: const [], + ), + isActive: true, + state: handleStepState(4), + ), + ], ), ], ), diff --git a/dev/conductor/ui/lib/widgets/substeps.dart b/dev/conductor/ui/lib/widgets/substeps.dart new file mode 100644 index 0000000000..ee49f6f0cc --- /dev/null +++ b/dev/conductor/ui/lib/widgets/substeps.dart @@ -0,0 +1,89 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +/// Group and display all substeps within a step into a widget. +/// +/// When all substeps are checked, [nextStep] can be executed to proceed to the next step. +class ConductorSubsteps extends StatefulWidget { + const ConductorSubsteps({ + Key? key, + required this.nextStep, + }) : super(key: key); + + final VoidCallback nextStep; + + @override + ConductorSubstepsState createState() => ConductorSubstepsState(); + + static const List _substepTitles = [ + 'Substep 1', + 'Substep 2', + 'Substep 3', + ]; +} + +class ConductorSubstepsState extends State { + List substepChecked = List.filled(ConductorSubsteps._substepTitles.length, false); + bool _nextStepPressed = false; + + // Hide the continue button once it is pressed. + void tapped() { + setState(() => _nextStepPressed = true); + } + + // When substepChecked[0] is true, the first substep is checked. If it false, it is unchecked. + void substepPressed(int index) { + setState(() { + substepChecked[index] = !substepChecked[index]; + }); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + CheckboxListTile( + value: substepChecked[0], + onChanged: (bool? newValue) { + substepPressed(0); + }, + title: Text(ConductorSubsteps._substepTitles[0]), + controlAffinity: ListTileControlAffinity.leading, + activeColor: Colors.grey, + selected: substepChecked[0], + ), + CheckboxListTile( + value: substepChecked[1], + onChanged: (bool? newValue) { + substepPressed(1); + }, + title: Text(ConductorSubsteps._substepTitles[1]), + controlAffinity: ListTileControlAffinity.leading, + activeColor: Colors.grey, + selected: substepChecked[1], + ), + CheckboxListTile( + value: substepChecked[2], + onChanged: (bool? newValue) { + substepPressed(2); + }, + title: Text(ConductorSubsteps._substepTitles[2]), + controlAffinity: ListTileControlAffinity.leading, + activeColor: Colors.grey, + selected: substepChecked[2], + ), + if (!substepChecked.contains(false) && !_nextStepPressed) + ElevatedButton( + onPressed: () { + tapped(); + widget.nextStep(); + }, + child: const Text('Continue'), + ), + ], + ); + } +} diff --git a/dev/conductor/ui/test/widgets/stepper_test.dart b/dev/conductor/ui/test/widgets/stepper_test.dart new file mode 100644 index 0000000000..daa1638045 --- /dev/null +++ b/dev/conductor/ui/test/widgets/stepper_test.dart @@ -0,0 +1,86 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:conductor_ui/widgets/progression.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets( + 'All substeps of the current step must be checked before able to continue to the next step', + (WidgetTester tester) async { + await tester.pumpWidget( + StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return MaterialApp( + home: Material( + child: Column( + children: const [ + MainProgression( + releaseState: null, + stateFilePath: './testPath', + ), + ], + ), + ), + ); + }, + ), + ); + + expect(find.byType(Stepper), findsOneWidget); + expect(find.text('Initialize a New Flutter Release'), findsOneWidget); + expect(find.text('Continue'), findsNWidgets(0)); + + await tester.tap(find.text('Substep 1').first); + await tester.tap(find.text('Substep 2').first); + await tester.pumpAndSettle(); + expect(find.text('Continue'), findsNWidgets(0)); + + await tester.tap(find.text('Substep 3').first); + await tester.pumpAndSettle(); + expect(find.text('Continue'), findsOneWidget); + expect(tester.widget(find.byType(Stepper)).steps[0].state, equals(StepState.indexed)); + expect(tester.widget(find.byType(Stepper)).steps[1].state, equals(StepState.disabled)); + + await tester.tap(find.text('Continue')); + await tester.pumpAndSettle(); + expect(tester.widget(find.byType(Stepper)).steps[0].state, + equals(StepState.complete)); + expect(tester.widget(find.byType(Stepper)).steps[1].state, + equals(StepState.indexed)); + }); + + testWidgets('When user clicks on a previously completed step, Stepper does not navigate back.', + (WidgetTester tester) async { + await tester.pumpWidget( + StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return MaterialApp( + home: Material( + child: Column( + children: const [ + MainProgression( + releaseState: null, + stateFilePath: './testPath', + ), + ], + ), + ), + ); + }, + ), + ); + + await tester.tap(find.text('Substep 1').first); + await tester.tap(find.text('Substep 2').first); + await tester.tap(find.text('Substep 3').first); + await tester.pumpAndSettle(); + await tester.tap(find.text('Continue')); + await tester.tap(find.text('Initialize a New Flutter Release')); + await tester.pumpAndSettle(); + + expect(tester.widget(find.byType(Stepper)).currentStep, equals(1)); + }); +}