Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Tour of Beam] [Frontend] Content tree URLs #23776

Merged
merged 11 commits into from
Nov 16, 2022
Merged
20 changes: 15 additions & 5 deletions learning/tour-of-beam/frontend/lib/models/content_tree.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,29 @@

import '../repositories/models/get_content_tree_response.dart';
import 'module.dart';
import 'node.dart';
import 'parent_node.dart';

class ContentTreeModel {
final String sdkId;
class ContentTreeModel extends ParentNodeModel {
final List<ModuleModel> modules;

String get sdkId => id;

@override
List<NodeModel> get nodes => modules;

const ContentTreeModel({
required this.sdkId,
required super.id,
required this.modules,
});
}) : super(
parent: null,
title: '',
nodes: modules,
);

ContentTreeModel.fromResponse(GetContentTreeResponse response)
: this(
sdkId: response.sdkId,
id: response.sdkId,
modules: response.modules
.map(ModuleModel.fromResponse)
.toList(growable: false),
Expand Down
29 changes: 21 additions & 8 deletions learning/tour-of-beam/frontend/lib/models/group.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,28 @@ import 'parent_node.dart';
class GroupModel extends ParentNodeModel {
const GroupModel({
required super.id,
required super.title,
required super.nodes,
required super.parent,
required super.title,
});

GroupModel.fromResponse(GroupResponseModel group)
: super(
id: group.id,
title: group.title,
nodes:
group.nodes.map(NodeModel.fromResponse).toList(growable: false),
);
factory GroupModel.fromResponse(
GroupResponseModel groupResponse,
ParentNodeModel parent,
) {
final group = GroupModel(
id: groupResponse.id,
nodes: [],
parent: parent,
title: groupResponse.title,
);

group.nodes.addAll(
groupResponse.nodes.map<NodeModel>(
(node) => NodeModel.fromResponse(node, group),
),
);

return group;
}
}
29 changes: 19 additions & 10 deletions learning/tour-of-beam/frontend/lib/models/module.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,27 @@ class ModuleModel extends ParentNodeModel {

const ModuleModel({
required super.id,
required super.title,
required super.nodes,
required super.parent,
required super.title,
required this.complexity,
});

ModuleModel.fromResponse(ModuleResponseModel module)
: complexity = module.complexity,
super(
id: module.id,
title: module.title,
nodes: module.nodes
.map<NodeModel>(NodeModel.fromResponse)
.toList(growable: false),
);
factory ModuleModel.fromResponse(ModuleResponseModel moduleResponse) {
final module = ModuleModel(
complexity: moduleResponse.complexity,
nodes: [],
id: moduleResponse.id,
parent: null,
title: moduleResponse.title,
);

module.nodes.addAll(
moduleResponse.nodes.map<NodeModel>(
(node) => NodeModel.fromResponse(node, module),
),
);

return module;
}
}
20 changes: 15 additions & 5 deletions learning/tour-of-beam/frontend/lib/models/node.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,18 @@
import '../repositories/models/node.dart';
import '../repositories/models/node_type_enum.dart';
import 'group.dart';
import 'parent_node.dart';
import 'unit.dart';

abstract class NodeModel {
final String id;
final String title;
final NodeModel? parent;

const NodeModel({
required this.id,
required this.title,
required this.parent,
});

/// Constructs nodes from the response data.
Expand All @@ -36,20 +39,27 @@ abstract class NodeModel {
/// because they come from a golang backend which does not
/// support inheritance, and so they use an extra layer of composition
/// which is inconvenient in Flutter.
static List<NodeModel> fromMaps(List json) {
static List<NodeModel> fromMaps(List json, ParentNodeModel parent) {
return json
.cast<Map<String, dynamic>>()
.map<NodeResponseModel>(NodeResponseModel.fromJson)
.map(fromResponse)
.map((nodeResponse) => fromResponse(nodeResponse, parent))
.toList();
}

static NodeModel fromResponse(NodeResponseModel node) {
static NodeModel fromResponse(
NodeResponseModel node,
ParentNodeModel parent,
) {
switch (node.type) {
case NodeType.group:
return GroupModel.fromResponse(node.group!);
return GroupModel.fromResponse(node.group!, parent);
case NodeType.unit:
return UnitModel.fromResponse(node.unit!);
return UnitModel.fromResponse(node.unit!, parent);
}
}

NodeModel getFirstUnit();

NodeModel? getNodeByTreeIds(List<String> treeIds);
}
18 changes: 18 additions & 0 deletions learning/tour-of-beam/frontend/lib/models/parent_node.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,32 @@
* limitations under the License.
*/

import 'package:collection/collection.dart';

import 'node.dart';

abstract class ParentNodeModel extends NodeModel {
final List<NodeModel> nodes;

const ParentNodeModel({
required super.id,
required super.parent,
required super.title,
required this.nodes,
});

@override
NodeModel getFirstUnit() => nodes[0].getFirstUnit();

@override
NodeModel? getNodeByTreeIds(List<String> treeIds) {
final firstId = treeIds.firstOrNull;
final child = nodes.firstWhereOrNull((node) => node.id == firstId);

if (child == null) {
return null;
}

return child.getNodeByTreeIds(treeIds.sublist(1));
}
}
15 changes: 13 additions & 2 deletions learning/tour-of-beam/frontend/lib/models/unit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,19 @@

import '../repositories/models/unit.dart';
import 'node.dart';
import 'parent_node.dart';

class UnitModel extends NodeModel {
UnitModel.fromResponse(UnitResponseModel unit)
: super(id: unit.id, title: unit.title);
UnitModel.fromResponse(UnitResponseModel unit, ParentNodeModel parent)
: super(
id: unit.id,
parent: parent,
title: unit.title,
);

@override
NodeModel getFirstUnit() => this;

@override
NodeModel? getNodeByTreeIds(List<String> treeIds) => this;
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,33 +17,96 @@
*/

import 'package:flutter/widgets.dart';
import 'package:get_it/get_it.dart';
import 'package:playground_components/playground_components.dart';

import '../../../cache/content_tree.dart';
import '../../../models/group.dart';
import '../../../models/node.dart';
import '../../../models/unit.dart';

class ContentTreeController extends ChangeNotifier {
String _sdkId;
List<String> _treeIds;
NodeModel? _currentNode;
final _contentTreeCache = GetIt.instance.get<ContentTreeCache>();
final _expandedIds = <String>{};

Set<String> get expandedIds => _expandedIds;

ContentTreeController({
required String initialSdkId,
List<String> initialTreeIds = const [],
}) : _sdkId = initialSdkId,
_treeIds = initialTreeIds;
_treeIds = initialTreeIds {
_expandedIds.addAll(initialTreeIds);

_contentTreeCache.addListener(_onContentTreeCacheChange);
_onContentTreeCacheChange();
}

Sdk get sdk => Sdk.parseOrCreate(_sdkId);
String get sdkId => _sdkId;
List<String> get treeIds => _treeIds;
NodeModel? get currentNode => _currentNode;

void onNodeTap(NodeModel node) {
void openNode(NodeModel node) {
if (!_expandedIds.contains(node.id)) {
_expandedIds.add(node.id);
}

if (node == _currentNode) {
return;
}

_currentNode = node;
// TODO(alexeyinkin): Set _treeIds from node.
if (node is GroupModel) {
openNode(node.nodes.first);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return after this line would cut redundant notifyListeners() calls.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then state is not updated when needed.

} else if (node is UnitModel) {
_currentNode = node;
}

if (_currentNode != null) {
_treeIds = _getNodeAncestors(_currentNode!, [_currentNode!.id]);
}
notifyListeners();
}

void expandGroup(GroupModel group) {
_expandedIds.add(group.id);
notifyListeners();
}

void collapseGroup(GroupModel group) {
_expandedIds.remove(group.id);
notifyListeners();
}

List<String> _getNodeAncestors(NodeModel node, List<String> ancestorIds) {
if (node.parent != null) {
return _getNodeAncestors(
node.parent!,
[...ancestorIds, node.parent!.id],
);
}
return ancestorIds.reversed.toList();
}

void _onContentTreeCacheChange() {
final contentTree = _contentTreeCache.getContentTree(_sdkId);
if (contentTree == null) {
return;
}

openNode(
contentTree.getNodeByTreeIds(_treeIds) ?? contentTree.getFirstUnit(),
);

notifyListeners();
}

@override
void dispose() {
_contentTreeCache.removeListener(_onContentTreeCacheChange);
super.dispose();
}
}
9 changes: 7 additions & 2 deletions learning/tour-of-beam/frontend/lib/pages/tour/path.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class TourPath extends PagePath {
final String sdkId;
final List<String> treeIds;

static final _regExp = RegExp(r'^/tour/([a-z]+)(/[/-a-zA-Z0-9]+)?$');
static final _regExp = RegExp(r'^/tour/([a-z]+)((/[-a-zA-Z0-9]+)*)$');

TourPath({
required this.sdkId,
Expand All @@ -47,7 +47,12 @@ class TourPath extends PagePath {
if (matches == null) return null;

final sdkId = matches[1] ?? (throw Error());
final treeIds = matches[2]?.split('/') ?? const [];
final treeIdsString = matches[2];

final treeIds = (treeIdsString == null)
? const <String>[]
// TODO(nausharipov): use RegExp to remove the slash
: treeIdsString.substring(1).split('/');

return TourPath(
sdkId: sdkId,
Expand Down
2 changes: 2 additions & 0 deletions learning/tour-of-beam/frontend/lib/pages/tour/state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class TourNotifier extends ChangeNotifier with PageStateMixin<void> {
playgroundController = _createPlaygroundController(initialSdkId) {
contentTreeController.addListener(_onChanged);
_unitContentCache.addListener(_onChanged);
_onChanged();
}

@override
Expand All @@ -53,6 +54,7 @@ class TourNotifier extends ChangeNotifier with PageStateMixin<void> {
);

void _onChanged() {
emitPathChanged();
final currentNode = contentTreeController.currentNode;
if (currentNode is UnitModel) {
final content = _unitContentCache.getUnitContent(
Expand Down
Loading