Alex chen conductor ui3 (#91118)
This commit is contained in:
parent
46a52d03bd
commit
3c6c2aa50a
33
dev/conductor/ui/lib/widgets/conductor_status.dart
Normal file
33
dev/conductor/ui/lib/widgets/conductor_status.dart
Normal file
@ -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<ConductorStatus> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SelectableText(
|
||||
widget.releaseState != null
|
||||
? presentState(widget.releaseState!)
|
||||
: 'No persistent state file found at ${widget.stateFilePath}',
|
||||
);
|
||||
}
|
||||
}
|
@ -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<String> _stepTitles = <String>[
|
||||
'Initialize a New Flutter Release',
|
||||
'Flutter Engine Cherrypicks',
|
||||
'Flutter Framework Cherrypicks',
|
||||
'Publish the Release',
|
||||
'Release is Successfully published'
|
||||
];
|
||||
}
|
||||
|
||||
class MainProgressionState extends State<MainProgression> {
|
||||
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: <Widget>[
|
||||
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 <Widget>[],
|
||||
);
|
||||
},
|
||||
type: StepperType.vertical,
|
||||
physics: const ScrollPhysics(),
|
||||
currentStep: _completedStep,
|
||||
onStepContinue: nextStep,
|
||||
steps: <Step>[
|
||||
Step(
|
||||
title: Text(MainProgression._stepTitles[0]),
|
||||
content: Column(
|
||||
children: <Widget>[
|
||||
ConductorSubsteps(nextStep: nextStep),
|
||||
],
|
||||
),
|
||||
isActive: true,
|
||||
state: handleStepState(0),
|
||||
),
|
||||
Step(
|
||||
title: Text(MainProgression._stepTitles[1]),
|
||||
content: Column(
|
||||
children: <Widget>[
|
||||
ConductorSubsteps(nextStep: nextStep),
|
||||
],
|
||||
),
|
||||
isActive: true,
|
||||
state: handleStepState(1),
|
||||
),
|
||||
Step(
|
||||
title: Text(MainProgression._stepTitles[2]),
|
||||
content: Column(
|
||||
children: <Widget>[
|
||||
ConductorSubsteps(nextStep: nextStep),
|
||||
],
|
||||
),
|
||||
isActive: true,
|
||||
state: handleStepState(2),
|
||||
),
|
||||
Step(
|
||||
title: Text(MainProgression._stepTitles[3]),
|
||||
content: Column(
|
||||
children: <Widget>[
|
||||
ConductorSubsteps(nextStep: nextStep),
|
||||
],
|
||||
),
|
||||
isActive: true,
|
||||
state: handleStepState(3),
|
||||
),
|
||||
Step(
|
||||
title: Text(MainProgression._stepTitles[4]),
|
||||
content: Column(
|
||||
children: const <Widget>[],
|
||||
),
|
||||
isActive: true,
|
||||
state: handleStepState(4),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
89
dev/conductor/ui/lib/widgets/substeps.dart
Normal file
89
dev/conductor/ui/lib/widgets/substeps.dart
Normal file
@ -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<String> _substepTitles = <String>[
|
||||
'Substep 1',
|
||||
'Substep 2',
|
||||
'Substep 3',
|
||||
];
|
||||
}
|
||||
|
||||
class ConductorSubstepsState extends State<ConductorSubsteps> {
|
||||
List<bool> substepChecked = List<bool>.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: <Widget>[
|
||||
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'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
86
dev/conductor/ui/test/widgets/stepper_test.dart
Normal file
86
dev/conductor/ui/test/widgets/stepper_test.dart
Normal file
@ -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 <Widget>[
|
||||
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<Stepper>(find.byType(Stepper)).steps[0].state, equals(StepState.indexed));
|
||||
expect(tester.widget<Stepper>(find.byType(Stepper)).steps[1].state, equals(StepState.disabled));
|
||||
|
||||
await tester.tap(find.text('Continue'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.widget<Stepper>(find.byType(Stepper)).steps[0].state,
|
||||
equals(StepState.complete));
|
||||
expect(tester.widget<Stepper>(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 <Widget>[
|
||||
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<Stepper>(find.byType(Stepper)).currentStep, equals(1));
|
||||
});
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user