mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
integrate board plugin into document (#1675)
* fix: cursor doesn't blink when opening selection menu * feat: add board plugin * feat: integrate board plugin into document * feat: add i10n and fix known bugs * feat: support jump to board page on document * feat: disable editor scroll only when the board plugin is selected * chore: dart fix * chore: remove unused files * fix: dart lint
This commit is contained in:
parent
0d8adaa921
commit
5de3912fe3
@ -314,12 +314,18 @@
|
||||
"date": {
|
||||
"timeHintTextInTwelveHour": "01:00 PM",
|
||||
"timeHintTextInTwentyFourHour": "13:00"
|
||||
},
|
||||
"slashMenu": {
|
||||
"board": {
|
||||
"selectABoardToLinkTo": "Select a board to link to"
|
||||
}
|
||||
}
|
||||
},
|
||||
"board": {
|
||||
"column": {
|
||||
"create_new_card": "New"
|
||||
}
|
||||
},
|
||||
"menuName": "Board"
|
||||
},
|
||||
"calendar": {
|
||||
"menuName": "Calendar",
|
||||
|
@ -1,8 +1,10 @@
|
||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
||||
import 'package:app_flowy/plugins/util.dart';
|
||||
import 'package:app_flowy/startup/plugin/plugin.dart';
|
||||
import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
|
||||
import 'package:app_flowy/workspace/presentation/widgets/left_bar_item.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:app_flowy/startup/plugin/plugin.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'presentation/board_page.dart';
|
||||
@ -18,7 +20,7 @@ class BoardPluginBuilder implements PluginBuilder {
|
||||
}
|
||||
|
||||
@override
|
||||
String get menuName => "Board";
|
||||
String get menuName => LocaleKeys.board_menuName.tr();
|
||||
|
||||
@override
|
||||
String get menuIcon => "editor/board";
|
||||
|
@ -4,34 +4,40 @@ import 'dart:collection';
|
||||
|
||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
||||
import 'package:app_flowy/plugins/board/application/card/card_data_controller.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/row/row_data_controller.dart';
|
||||
import 'package:app_flowy/plugins/grid/presentation/widgets/cell/cell_builder.dart';
|
||||
import 'package:app_flowy/plugins/grid/presentation/widgets/row/row_detail.dart';
|
||||
import 'package:appflowy_board/appflowy_board.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
|
||||
import 'package:flowy_infra_ui/widget/error_page.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-grid/field_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-grid/row_entities.pb.dart';
|
||||
import 'package:appflowy_board/appflowy_board.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/error_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../application/board_bloc.dart';
|
||||
import 'card/card.dart';
|
||||
import 'card/card_cell_builder.dart';
|
||||
import 'toolbar/board_toolbar.dart';
|
||||
|
||||
class BoardPage extends StatelessWidget {
|
||||
final ViewPB view;
|
||||
BoardPage({
|
||||
required this.view,
|
||||
Key? key,
|
||||
this.onEditStateChanged,
|
||||
}) : super(key: ValueKey(view.id));
|
||||
|
||||
final ViewPB view;
|
||||
|
||||
/// Called when edit state changed
|
||||
final VoidCallback? onEditStateChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
@ -45,7 +51,9 @@ class BoardPage extends StatelessWidget {
|
||||
const Center(child: CircularProgressIndicator.adaptive()),
|
||||
finish: (result) {
|
||||
return result.successOrFail.fold(
|
||||
(_) => const BoardContent(),
|
||||
(_) => BoardContent(
|
||||
onEditStateChanged: onEditStateChanged,
|
||||
),
|
||||
(err) => FlowyErrorPage(err.toString()),
|
||||
);
|
||||
},
|
||||
@ -57,7 +65,12 @@ class BoardPage extends StatelessWidget {
|
||||
}
|
||||
|
||||
class BoardContent extends StatefulWidget {
|
||||
const BoardContent({Key? key}) : super(key: key);
|
||||
const BoardContent({
|
||||
Key? key,
|
||||
this.onEditStateChanged,
|
||||
}) : super(key: key);
|
||||
|
||||
final VoidCallback? onEditStateChanged;
|
||||
|
||||
@override
|
||||
State<BoardContent> createState() => _BoardContentState();
|
||||
@ -79,7 +92,10 @@ class _BoardContentState extends State<BoardContent> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocListener<BoardBloc, BoardState>(
|
||||
listener: (context, state) => _handleEditStateChanged(state, context),
|
||||
listener: (context, state) {
|
||||
_handleEditStateChanged(state, context);
|
||||
widget.onEditStateChanged?.call();
|
||||
},
|
||||
child: BlocBuilder<BoardBloc, BoardState>(
|
||||
buildWhen: (previous, current) => previous.groupIds != current.groupIds,
|
||||
builder: (context, state) {
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'package:app_flowy/plugins/document/presentation/plugins/board/board_menu_item.dart';
|
||||
import 'package:app_flowy/plugins/document/presentation/plugins/board/board_node_widget.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||
@ -97,7 +99,6 @@ class _DocumentPageState extends State<DocumentPage> {
|
||||
|
||||
Widget _renderAppFlowyEditor(EditorState editorState) {
|
||||
final theme = Theme.of(context);
|
||||
final editorMaxWidth = MediaQuery.of(context).size.width * 0.6;
|
||||
final editor = AppFlowyEditor(
|
||||
editorState: editorState,
|
||||
autoFocus: editorState.document.isEmpty,
|
||||
@ -108,6 +109,8 @@ class _DocumentPageState extends State<DocumentPage> {
|
||||
kMathEquationType: MathEquationNodeWidgetBuidler(),
|
||||
// Code Block
|
||||
kCodeBlockType: CodeBlockNodeWidgetBuilder(),
|
||||
// Board
|
||||
kBoardType: BoardNodeWidgetBuilder(),
|
||||
// Card
|
||||
kCalloutType: CalloutNodeWidgetBuilder(),
|
||||
},
|
||||
@ -128,6 +131,8 @@ class _DocumentPageState extends State<DocumentPage> {
|
||||
codeBlockMenuItem,
|
||||
// Emoji
|
||||
emojiMenuItem,
|
||||
// Board
|
||||
boardMenuItem,
|
||||
],
|
||||
themeData: theme.copyWith(extensions: [
|
||||
...theme.extensions.values,
|
||||
@ -138,8 +143,8 @@ class _DocumentPageState extends State<DocumentPage> {
|
||||
return Expanded(
|
||||
child: Center(
|
||||
child: Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: editorMaxWidth,
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: double.infinity,
|
||||
),
|
||||
child: editor,
|
||||
),
|
||||
|
@ -9,7 +9,7 @@ EditorStyle customEditorTheme(BuildContext context) {
|
||||
? EditorStyle.dark
|
||||
: EditorStyle.light;
|
||||
editorStyle = editorStyle.copyWith(
|
||||
padding: const EdgeInsets.all(0),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 40),
|
||||
textStyle: editorStyle.textStyle?.copyWith(
|
||||
fontFamily: 'poppins',
|
||||
fontSize: documentStyle.fontSize,
|
||||
|
@ -0,0 +1,195 @@
|
||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
||||
import 'package:app_flowy/plugins/document/presentation/plugins/board/board_node_widget.dart';
|
||||
import 'package:app_flowy/workspace/application/app/app_service.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/app.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:dartz/dartz.dart' as dartz;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
SelectionMenuItem boardMenuItem = SelectionMenuItem(
|
||||
name: () => LocaleKeys.board_menuName.tr(),
|
||||
icon: (editorState, onSelected) {
|
||||
return svgWidget(
|
||||
'editor/board',
|
||||
size: const Size.square(18.0),
|
||||
color: onSelected
|
||||
? editorState.editorStyle.selectionMenuItemSelectedIconColor
|
||||
: editorState.editorStyle.selectionMenuItemIconColor,
|
||||
);
|
||||
},
|
||||
keywords: ['board'],
|
||||
handler: _showLinkToPageMenu,
|
||||
);
|
||||
|
||||
EditorState? _editorState;
|
||||
OverlayEntry? _linkToPageMenu;
|
||||
void _dismissLinkToPageMenu() {
|
||||
_linkToPageMenu?.remove();
|
||||
_linkToPageMenu = null;
|
||||
|
||||
_editorState?.service.selectionService.currentSelection
|
||||
.removeListener(_dismissLinkToPageMenu);
|
||||
_editorState = null;
|
||||
}
|
||||
|
||||
void _showLinkToPageMenu(
|
||||
EditorState editorState,
|
||||
SelectionMenuService menuService,
|
||||
BuildContext context,
|
||||
) {
|
||||
final aligment = menuService.alignment;
|
||||
final offset = menuService.offset;
|
||||
menuService.dismiss();
|
||||
|
||||
_editorState = editorState;
|
||||
|
||||
_linkToPageMenu?.remove();
|
||||
_linkToPageMenu = OverlayEntry(builder: (context) {
|
||||
return Positioned(
|
||||
top: aligment == Alignment.bottomLeft ? offset.dy : null,
|
||||
bottom: aligment == Alignment.topLeft ? offset.dy : null,
|
||||
left: offset.dx,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: LinkToPageMenu(
|
||||
editorState: editorState,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
Overlay.of(context)?.insert(_linkToPageMenu!);
|
||||
|
||||
editorState.service.selectionService.currentSelection
|
||||
.addListener(_dismissLinkToPageMenu);
|
||||
}
|
||||
|
||||
class LinkToPageMenu extends StatefulWidget {
|
||||
final EditorState editorState;
|
||||
|
||||
const LinkToPageMenu({
|
||||
super.key,
|
||||
required this.editorState,
|
||||
});
|
||||
|
||||
@override
|
||||
State<LinkToPageMenu> createState() => _LinkToPageMenuState();
|
||||
}
|
||||
|
||||
class _LinkToPageMenuState extends State<LinkToPageMenu> {
|
||||
EditorStyle get style => widget.editorState.editorStyle;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: Colors.transparent,
|
||||
width: 300,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.fromLTRB(10, 6, 10, 6),
|
||||
decoration: BoxDecoration(
|
||||
color: style.selectionMenuBackgroundColor,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
blurRadius: 5,
|
||||
spreadRadius: 1,
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
),
|
||||
],
|
||||
borderRadius: BorderRadius.circular(6.0),
|
||||
),
|
||||
child: _buildBoardListWidget(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<dartz.Tuple2<AppPB, List<ViewPB>>>> fetchBoards() async {
|
||||
return AppService().fetchViews(ViewLayoutTypePB.Board);
|
||||
}
|
||||
|
||||
Widget _buildBoardListWidget(BuildContext context) {
|
||||
return FutureBuilder<List<dartz.Tuple2<AppPB, List<ViewPB>>>>(
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData &&
|
||||
snapshot.connectionState == ConnectionState.done) {
|
||||
final apps = snapshot.data;
|
||||
final children = <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: FlowyText.regular(
|
||||
LocaleKeys.document_slashMenu_board_selectABoardToLinkTo.tr(),
|
||||
fontSize: 10,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
];
|
||||
if (apps != null && apps.isNotEmpty) {
|
||||
for (final app in apps) {
|
||||
if (app.value2.isNotEmpty) {
|
||||
children.add(
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: FlowyText.regular(
|
||||
app.value1.name,
|
||||
),
|
||||
),
|
||||
);
|
||||
for (final board in app.value2) {
|
||||
children.add(
|
||||
FlowyButton(
|
||||
leftIcon: svgWidget(
|
||||
'editor/board',
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
text: FlowyText.regular(board.name),
|
||||
onTap: () => widget.editorState.insertBoard(
|
||||
app.value1,
|
||||
board,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: children,
|
||||
);
|
||||
} else {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
},
|
||||
future: fetchBoards(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension on EditorState {
|
||||
void insertBoard(AppPB appPB, ViewPB viewPB) {
|
||||
final selection = service.selectionService.currentSelection.value;
|
||||
final textNodes =
|
||||
service.selectionService.currentSelectedNodes.whereType<TextNode>();
|
||||
if (selection == null || textNodes.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final transaction = this.transaction;
|
||||
transaction.insertNode(
|
||||
selection.end.path,
|
||||
Node(
|
||||
type: kBoardType,
|
||||
attributes: {
|
||||
kAppID: appPB.id,
|
||||
kBoardID: viewPB.id,
|
||||
},
|
||||
),
|
||||
);
|
||||
apply(transaction);
|
||||
}
|
||||
}
|
@ -0,0 +1,175 @@
|
||||
import 'package:app_flowy/plugins/board/presentation/board_page.dart';
|
||||
import 'package:app_flowy/startup/startup.dart';
|
||||
import 'package:app_flowy/workspace/application/app/app_service.dart';
|
||||
import 'package:app_flowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
|
||||
import 'package:app_flowy/workspace/presentation/home/menu/menu.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:dartz/dartz.dart' as dartz;
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const String kBoardType = 'board';
|
||||
const String kAppID = 'app_id';
|
||||
const String kBoardID = 'board_id';
|
||||
|
||||
class BoardNodeWidgetBuilder extends NodeWidgetBuilder<Node> {
|
||||
@override
|
||||
Widget build(NodeWidgetContext<Node> context) {
|
||||
return _BoardWidget(
|
||||
key: context.node.key,
|
||||
node: context.node,
|
||||
editorState: context.editorState,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
NodeValidator<Node> get nodeValidator => (node) {
|
||||
return node.attributes[kBoardID] is String &&
|
||||
node.attributes[kAppID] is String;
|
||||
};
|
||||
}
|
||||
|
||||
class _BoardWidget extends StatefulWidget {
|
||||
const _BoardWidget({
|
||||
Key? key,
|
||||
required this.node,
|
||||
required this.editorState,
|
||||
}) : super(key: key);
|
||||
|
||||
final Node node;
|
||||
final EditorState editorState;
|
||||
|
||||
@override
|
||||
State<_BoardWidget> createState() => _BoardWidgetState();
|
||||
}
|
||||
|
||||
class _BoardWidgetState extends State<_BoardWidget> with SelectableMixin {
|
||||
RenderBox get _renderBox => context.findRenderObject() as RenderBox;
|
||||
|
||||
String get boardID {
|
||||
return widget.node.attributes[kBoardID];
|
||||
}
|
||||
|
||||
String get appID {
|
||||
return widget.node.attributes[kAppID];
|
||||
}
|
||||
|
||||
late Future<dartz.Either<ViewPB, FlowyError>> board;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
board = _fetchBoard();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<dartz.Either<ViewPB, FlowyError>>(
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
final board = snapshot.data?.getLeftOrNull<ViewPB>();
|
||||
if (board != null) {
|
||||
return _buildBoard(context, board);
|
||||
}
|
||||
}
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
},
|
||||
future: board,
|
||||
);
|
||||
}
|
||||
|
||||
Future<dartz.Either<ViewPB, FlowyError>> _fetchBoard() async {
|
||||
return AppService().getView(appID, boardID);
|
||||
}
|
||||
|
||||
Widget _buildBoard(BuildContext context, ViewPB viewPB) {
|
||||
return MouseRegion(
|
||||
onHover: (event) {
|
||||
if (widget.node.isSelected(widget.editorState)) {
|
||||
widget.editorState.service.scrollService?.disable();
|
||||
}
|
||||
},
|
||||
onExit: (event) {
|
||||
widget.editorState.service.scrollService?.enable();
|
||||
},
|
||||
child: SizedBox(
|
||||
height: 400,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 20,
|
||||
child: FlowyTextButton(
|
||||
viewPB.name,
|
||||
onPressed: () {
|
||||
getIt<MenuSharedState>().latestOpenView = viewPB;
|
||||
getIt<HomeStackManager>().setPlugin(viewPB.plugin());
|
||||
},
|
||||
),
|
||||
),
|
||||
BoardPage(
|
||||
key: ValueKey(viewPB.id),
|
||||
view: viewPB,
|
||||
onEditStateChanged: () {
|
||||
/// Clear selection when the edit state changes, otherwise the editor will prevent the keyboard event when the board is in edit mode.
|
||||
widget.editorState.service.selectionService.clearSelection();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get shouldCursorBlink => false;
|
||||
|
||||
@override
|
||||
CursorStyle get cursorStyle => CursorStyle.borderLine;
|
||||
|
||||
@override
|
||||
Position start() {
|
||||
return Position(path: widget.node.path, offset: 0);
|
||||
}
|
||||
|
||||
@override
|
||||
Position end() {
|
||||
return Position(path: widget.node.path, offset: 0);
|
||||
}
|
||||
|
||||
@override
|
||||
Position getPositionInOffset(Offset start) {
|
||||
return end();
|
||||
}
|
||||
|
||||
@override
|
||||
List<Rect> getRectsInSelection(Selection selection) {
|
||||
return [Offset.zero & _renderBox.size];
|
||||
}
|
||||
|
||||
@override
|
||||
Rect? getCursorRectInPosition(Position position) {
|
||||
final size = _renderBox.size;
|
||||
return Rect.fromLTWH(-size.width / 2.0, 0, size.width, size.height);
|
||||
}
|
||||
|
||||
@override
|
||||
Selection getSelectionInRange(Offset start, Offset end) {
|
||||
return Selection.single(
|
||||
path: widget.node.path,
|
||||
startOffset: 0,
|
||||
endOffset: 0,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Offset localToGlobal(Offset offset) {
|
||||
return _renderBox.localToGlobal(offset);
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
@ -77,4 +78,52 @@ class AppService {
|
||||
|
||||
return FolderEventMoveFolderItem(payload).send();
|
||||
}
|
||||
|
||||
Future<List<Tuple2<AppPB, List<ViewPB>>>> fetchViews(
|
||||
ViewLayoutTypePB layoutType) async {
|
||||
final result = <Tuple2<AppPB, List<ViewPB>>>[];
|
||||
return FolderEventReadCurrentWorkspace().send().then((value) async {
|
||||
final workspaces = value.getLeftOrNull<WorkspaceSettingPB>();
|
||||
if (workspaces != null) {
|
||||
final apps = workspaces.workspace.apps.items;
|
||||
for (var app in apps) {
|
||||
final views = await getViews(appId: app.id).then(
|
||||
(value) => value
|
||||
.getLeftOrNull<List<ViewPB>>()
|
||||
?.where((e) => e.layout == layoutType)
|
||||
.toList(),
|
||||
);
|
||||
if (views != null && views.isNotEmpty) {
|
||||
result.add(Tuple2(app, views));
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
Future<Either<ViewPB, FlowyError>> getView(
|
||||
String appID,
|
||||
String viewID,
|
||||
) async {
|
||||
final payload = AppIdPB.create()..value = appID;
|
||||
return FolderEventReadApp(payload).send().then((result) {
|
||||
return result.fold(
|
||||
(app) => left(
|
||||
app.belongings.items.firstWhere((e) => e.id == viewID),
|
||||
),
|
||||
(error) => right(error),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension AppFlowy on Either {
|
||||
T? getLeftOrNull<T>() {
|
||||
if (isLeft()) {
|
||||
final result = fold<T?>((l) => l, (r) => null);
|
||||
return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,7 @@
|
||||
]
|
||||
},
|
||||
{ "type": "text", "delta": [] },
|
||||
{ "type": "board" },
|
||||
{
|
||||
"type": "text",
|
||||
"delta": [
|
||||
|
@ -44,3 +44,4 @@ export 'src/plugins/markdown/document_markdown.dart';
|
||||
export 'src/plugins/quill_delta/delta_document_encoder.dart';
|
||||
export 'src/commands/text/text_commands.dart';
|
||||
export 'src/render/toolbar/toolbar_item.dart';
|
||||
export 'src/extensions/node_extensions.dart';
|
||||
|
@ -71,7 +71,7 @@ class Node extends ChangeNotifier with LinkedListEntry<Node> {
|
||||
Attributes _attributes;
|
||||
|
||||
// Renderable
|
||||
GlobalKey? key;
|
||||
final key = GlobalKey();
|
||||
final layerLink = LayerLink();
|
||||
|
||||
Attributes get attributes => {..._attributes};
|
||||
|
@ -1,17 +1,18 @@
|
||||
import 'package:appflowy_editor/src/core/document/node.dart';
|
||||
import 'package:appflowy_editor/src/core/document/path.dart';
|
||||
import 'package:appflowy_editor/src/core/location/selection.dart';
|
||||
import 'package:appflowy_editor/src/editor_state.dart';
|
||||
import 'package:appflowy_editor/src/extensions/object_extensions.dart';
|
||||
import 'package:appflowy_editor/src/render/selection/selectable.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
extension NodeExtensions on Node {
|
||||
RenderBox? get renderBox =>
|
||||
key?.currentContext?.findRenderObject()?.unwrapOrNull<RenderBox>();
|
||||
key.currentContext?.findRenderObject()?.unwrapOrNull<RenderBox>();
|
||||
|
||||
BuildContext? get context => key?.currentContext;
|
||||
BuildContext? get context => key.currentContext;
|
||||
SelectableMixin? get selectable =>
|
||||
key?.currentState?.unwrapOrNull<SelectableMixin>();
|
||||
key.currentState?.unwrapOrNull<SelectableMixin>();
|
||||
|
||||
bool inSelection(Selection selection) {
|
||||
if (selection.start.path <= selection.end.path) {
|
||||
@ -28,4 +29,11 @@ extension NodeExtensions on Node {
|
||||
}
|
||||
return Rect.zero;
|
||||
}
|
||||
|
||||
bool isSelected(EditorState editorState) {
|
||||
final currentSelectedNodes =
|
||||
editorState.service.selectionService.currentSelectedNodes;
|
||||
return currentSelectedNodes.length == 1 &&
|
||||
currentSelectedNodes.first == this;
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor/src/extensions/node_extensions.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
ShortcutEventHandler cursorLeftSelect = (editorState, event) {
|
||||
|
@ -74,8 +74,6 @@ class AppFlowyRenderPlugin extends AppFlowyRenderPluginService {
|
||||
node.subtype == null ? node.type : '${node.type}/${node.subtype!}';
|
||||
final builder = _builders[name];
|
||||
if (builder != null && builder.nodeValidator(node)) {
|
||||
final key = GlobalKey(debugLabel: name);
|
||||
node.key = key;
|
||||
return _autoUpdateNodeWidget(builder, context);
|
||||
} else {
|
||||
// Returns a SizeBox with 0 height if no builder found.
|
||||
|
@ -1,28 +1,18 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:appflowy_editor/src/extensions/node_extensions.dart';
|
||||
|
||||
class MockNode extends Mock implements Node {}
|
||||
|
||||
void main() {
|
||||
final mockNode = MockNode();
|
||||
|
||||
group('NodeExtensions::', () {
|
||||
final selection = Selection(
|
||||
start: Position(path: [0]),
|
||||
end: Position(path: [1]),
|
||||
);
|
||||
|
||||
test('rect - renderBox is null', () {
|
||||
when(mockNode.renderBox).thenReturn(null);
|
||||
final result = mockNode.rect;
|
||||
expect(result, Rect.zero);
|
||||
});
|
||||
|
||||
test('inSelection', () {
|
||||
// I use an empty implementation instead of mock, because the mocked
|
||||
// version throws error trying to access the path.
|
||||
|
@ -43,7 +43,7 @@ void main() async {
|
||||
final selection =
|
||||
Selection.single(path: [0], startOffset: 0, endOffset: text.length);
|
||||
var node = editor.nodeAtPath([0]) as TextNode;
|
||||
var state = node.key?.currentState as DefaultSelectable;
|
||||
var state = node.key.currentState as DefaultSelectable;
|
||||
var checkboxWidget = find.byKey(state.iconKey!);
|
||||
await tester.tap(checkboxWidget);
|
||||
await tester.pumpAndSettle();
|
||||
@ -56,7 +56,7 @@ void main() async {
|
||||
expect(node.allSatisfyStrikethroughInSelection(selection), true);
|
||||
|
||||
node = editor.nodeAtPath([0]) as TextNode;
|
||||
state = node.key?.currentState as DefaultSelectable;
|
||||
state = node.key.currentState as DefaultSelectable;
|
||||
await tester.ensureVisible(find.byKey(state.iconKey!));
|
||||
await tester.tap(find.byKey(state.iconKey!));
|
||||
await tester.pump();
|
||||
|
@ -21,7 +21,7 @@ void main() async {
|
||||
await editor.startTesting();
|
||||
|
||||
final secondTextNode = editor.nodeAtPath([1]);
|
||||
final finder = find.byKey(secondTextNode!.key!);
|
||||
final finder = find.byKey(secondTextNode!.key);
|
||||
|
||||
final rect = tester.getRect(finder);
|
||||
// tap at the beginning
|
||||
@ -48,7 +48,7 @@ void main() async {
|
||||
await editor.startTesting();
|
||||
|
||||
final secondTextNode = editor.nodeAtPath([1]);
|
||||
final finder = find.byKey(secondTextNode!.key!);
|
||||
final finder = find.byKey(secondTextNode!.key);
|
||||
|
||||
final rect = tester.getRect(finder);
|
||||
// double tap
|
||||
@ -70,7 +70,7 @@ void main() async {
|
||||
await editor.startTesting();
|
||||
|
||||
final secondTextNode = editor.nodeAtPath([1]);
|
||||
final finder = find.byKey(secondTextNode!.key!);
|
||||
final finder = find.byKey(secondTextNode!.key);
|
||||
|
||||
final rect = tester.getRect(finder);
|
||||
// triple tap
|
||||
@ -93,7 +93,7 @@ void main() async {
|
||||
await editor.startTesting();
|
||||
|
||||
final secondTextNode = editor.nodeAtPath([1]) as TextNode;
|
||||
final finder = find.byKey(secondTextNode.key!);
|
||||
final finder = find.byKey(secondTextNode.key);
|
||||
|
||||
final rect = tester.getRect(finder);
|
||||
// secondary tap
|
||||
|
@ -47,7 +47,7 @@ SelectionMenuItem mathEquationMenuItem = SelectionMenuItem(
|
||||
final mathEquationState = editorState.document
|
||||
.nodeAtPath(mathEquationNodePath)
|
||||
?.key
|
||||
?.currentState;
|
||||
.currentState;
|
||||
if (mathEquationState != null &&
|
||||
mathEquationState is _MathEquationNodeWidgetState) {
|
||||
mathEquationState.showEditingDialog();
|
||||
|
@ -16,7 +16,7 @@ dependencies:
|
||||
path: ../appflowy_editor
|
||||
flowy_infra:
|
||||
path: ../flowy_infra
|
||||
flowy_infra_ui:
|
||||
flowy_infra_ui:
|
||||
path: ../flowy_infra_ui
|
||||
appflowy_popover:
|
||||
path: ../appflowy_popover
|
||||
|
Loading…
Reference in New Issue
Block a user