Skip to content

Commit 5de3912

Browse files
authored
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
1 parent 0d8adaa commit 5de3912

File tree

19 files changed

+486
-41
lines changed

19 files changed

+486
-41
lines changed

frontend/app_flowy/assets/translations/en.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,12 +314,18 @@
314314
"date": {
315315
"timeHintTextInTwelveHour": "01:00 PM",
316316
"timeHintTextInTwentyFourHour": "13:00"
317+
},
318+
"slashMenu": {
319+
"board": {
320+
"selectABoardToLinkTo": "Select a board to link to"
321+
}
317322
}
318323
},
319324
"board": {
320325
"column": {
321326
"create_new_card": "New"
322-
}
327+
},
328+
"menuName": "Board"
323329
},
324330
"calendar": {
325331
"menuName": "Calendar",

frontend/app_flowy/lib/plugins/board/board.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import 'package:app_flowy/generated/locale_keys.g.dart';
12
import 'package:app_flowy/plugins/util.dart';
3+
import 'package:app_flowy/startup/plugin/plugin.dart';
24
import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
35
import 'package:app_flowy/workspace/presentation/widgets/left_bar_item.dart';
46
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
5-
import 'package:app_flowy/startup/plugin/plugin.dart';
7+
import 'package:easy_localization/easy_localization.dart';
68
import 'package:flutter/material.dart';
79

810
import 'presentation/board_page.dart';
@@ -18,7 +20,7 @@ class BoardPluginBuilder implements PluginBuilder {
1820
}
1921

2022
@override
21-
String get menuName => "Board";
23+
String get menuName => LocaleKeys.board_menuName.tr();
2224

2325
@override
2426
String get menuIcon => "editor/board";

frontend/app_flowy/lib/plugins/board/presentation/board_page.dart

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,40 @@ import 'dart:collection';
44

55
import 'package:app_flowy/generated/locale_keys.g.dart';
66
import 'package:app_flowy/plugins/board/application/card/card_data_controller.dart';
7-
import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
87
import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
8+
import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
99
import 'package:app_flowy/plugins/grid/application/row/row_data_controller.dart';
1010
import 'package:app_flowy/plugins/grid/presentation/widgets/cell/cell_builder.dart';
1111
import 'package:app_flowy/plugins/grid/presentation/widgets/row/row_detail.dart';
12+
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
13+
import 'package:appflowy_backend/protobuf/flowy-grid/field_entities.pb.dart';
14+
import 'package:appflowy_backend/protobuf/flowy-grid/row_entities.pb.dart';
1215
import 'package:appflowy_board/appflowy_board.dart';
1316
import 'package:easy_localization/easy_localization.dart';
1417
import 'package:flowy_infra/image.dart';
15-
import 'package:flowy_infra_ui/style_widget/text.dart';
1618
import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
19+
import 'package:flowy_infra_ui/style_widget/text.dart';
1720
import 'package:flowy_infra_ui/widget/error_page.dart';
18-
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
19-
import 'package:appflowy_backend/protobuf/flowy-grid/field_entities.pb.dart';
20-
import 'package:appflowy_backend/protobuf/flowy-grid/row_entities.pb.dart';
2121
import 'package:flutter/material.dart';
2222
import 'package:flutter_bloc/flutter_bloc.dart';
23+
2324
import '../application/board_bloc.dart';
2425
import 'card/card.dart';
2526
import 'card/card_cell_builder.dart';
2627
import 'toolbar/board_toolbar.dart';
2728

2829
class BoardPage extends StatelessWidget {
29-
final ViewPB view;
3030
BoardPage({
3131
required this.view,
3232
Key? key,
33+
this.onEditStateChanged,
3334
}) : super(key: ValueKey(view.id));
3435

36+
final ViewPB view;
37+
38+
/// Called when edit state changed
39+
final VoidCallback? onEditStateChanged;
40+
3541
@override
3642
Widget build(BuildContext context) {
3743
return BlocProvider(
@@ -45,7 +51,9 @@ class BoardPage extends StatelessWidget {
4551
const Center(child: CircularProgressIndicator.adaptive()),
4652
finish: (result) {
4753
return result.successOrFail.fold(
48-
(_) => const BoardContent(),
54+
(_) => BoardContent(
55+
onEditStateChanged: onEditStateChanged,
56+
),
4957
(err) => FlowyErrorPage(err.toString()),
5058
);
5159
},
@@ -57,7 +65,12 @@ class BoardPage extends StatelessWidget {
5765
}
5866

5967
class BoardContent extends StatefulWidget {
60-
const BoardContent({Key? key}) : super(key: key);
68+
const BoardContent({
69+
Key? key,
70+
this.onEditStateChanged,
71+
}) : super(key: key);
72+
73+
final VoidCallback? onEditStateChanged;
6174

6275
@override
6376
State<BoardContent> createState() => _BoardContentState();
@@ -79,7 +92,10 @@ class _BoardContentState extends State<BoardContent> {
7992
@override
8093
Widget build(BuildContext context) {
8194
return BlocListener<BoardBloc, BoardState>(
82-
listener: (context, state) => _handleEditStateChanged(state, context),
95+
listener: (context, state) {
96+
_handleEditStateChanged(state, context);
97+
widget.onEditStateChanged?.call();
98+
},
8399
child: BlocBuilder<BoardBloc, BoardState>(
84100
buildWhen: (previous, current) => previous.groupIds != current.groupIds,
85101
builder: (context, state) {

frontend/app_flowy/lib/plugins/document/document_page.dart

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import 'package:app_flowy/plugins/document/presentation/plugins/board/board_menu_item.dart';
2+
import 'package:app_flowy/plugins/document/presentation/plugins/board/board_node_widget.dart';
13
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
24
import 'package:appflowy_editor/appflowy_editor.dart';
35
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
@@ -97,7 +99,6 @@ class _DocumentPageState extends State<DocumentPage> {
9799

98100
Widget _renderAppFlowyEditor(EditorState editorState) {
99101
final theme = Theme.of(context);
100-
final editorMaxWidth = MediaQuery.of(context).size.width * 0.6;
101102
final editor = AppFlowyEditor(
102103
editorState: editorState,
103104
autoFocus: editorState.document.isEmpty,
@@ -108,6 +109,8 @@ class _DocumentPageState extends State<DocumentPage> {
108109
kMathEquationType: MathEquationNodeWidgetBuidler(),
109110
// Code Block
110111
kCodeBlockType: CodeBlockNodeWidgetBuilder(),
112+
// Board
113+
kBoardType: BoardNodeWidgetBuilder(),
111114
// Card
112115
kCalloutType: CalloutNodeWidgetBuilder(),
113116
},
@@ -128,6 +131,8 @@ class _DocumentPageState extends State<DocumentPage> {
128131
codeBlockMenuItem,
129132
// Emoji
130133
emojiMenuItem,
134+
// Board
135+
boardMenuItem,
131136
],
132137
themeData: theme.copyWith(extensions: [
133138
...theme.extensions.values,
@@ -138,8 +143,8 @@ class _DocumentPageState extends State<DocumentPage> {
138143
return Expanded(
139144
child: Center(
140145
child: Container(
141-
constraints: BoxConstraints(
142-
maxWidth: editorMaxWidth,
146+
constraints: const BoxConstraints(
147+
maxWidth: double.infinity,
143148
),
144149
child: editor,
145150
),

frontend/app_flowy/lib/plugins/document/editor_styles.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ EditorStyle customEditorTheme(BuildContext context) {
99
? EditorStyle.dark
1010
: EditorStyle.light;
1111
editorStyle = editorStyle.copyWith(
12-
padding: const EdgeInsets.all(0),
12+
padding: const EdgeInsets.symmetric(horizontal: 40),
1313
textStyle: editorStyle.textStyle?.copyWith(
1414
fontFamily: 'poppins',
1515
fontSize: documentStyle.fontSize,
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import 'package:app_flowy/generated/locale_keys.g.dart';
2+
import 'package:app_flowy/plugins/document/presentation/plugins/board/board_node_widget.dart';
3+
import 'package:app_flowy/workspace/application/app/app_service.dart';
4+
import 'package:appflowy_backend/protobuf/flowy-folder/app.pb.dart';
5+
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
6+
import 'package:appflowy_editor/appflowy_editor.dart';
7+
import 'package:dartz/dartz.dart' as dartz;
8+
import 'package:easy_localization/easy_localization.dart';
9+
import 'package:flowy_infra/image.dart';
10+
import 'package:flowy_infra_ui/style_widget/button.dart';
11+
import 'package:flowy_infra_ui/style_widget/text.dart';
12+
import 'package:flutter/material.dart';
13+
14+
SelectionMenuItem boardMenuItem = SelectionMenuItem(
15+
name: () => LocaleKeys.board_menuName.tr(),
16+
icon: (editorState, onSelected) {
17+
return svgWidget(
18+
'editor/board',
19+
size: const Size.square(18.0),
20+
color: onSelected
21+
? editorState.editorStyle.selectionMenuItemSelectedIconColor
22+
: editorState.editorStyle.selectionMenuItemIconColor,
23+
);
24+
},
25+
keywords: ['board'],
26+
handler: _showLinkToPageMenu,
27+
);
28+
29+
EditorState? _editorState;
30+
OverlayEntry? _linkToPageMenu;
31+
void _dismissLinkToPageMenu() {
32+
_linkToPageMenu?.remove();
33+
_linkToPageMenu = null;
34+
35+
_editorState?.service.selectionService.currentSelection
36+
.removeListener(_dismissLinkToPageMenu);
37+
_editorState = null;
38+
}
39+
40+
void _showLinkToPageMenu(
41+
EditorState editorState,
42+
SelectionMenuService menuService,
43+
BuildContext context,
44+
) {
45+
final aligment = menuService.alignment;
46+
final offset = menuService.offset;
47+
menuService.dismiss();
48+
49+
_editorState = editorState;
50+
51+
_linkToPageMenu?.remove();
52+
_linkToPageMenu = OverlayEntry(builder: (context) {
53+
return Positioned(
54+
top: aligment == Alignment.bottomLeft ? offset.dy : null,
55+
bottom: aligment == Alignment.topLeft ? offset.dy : null,
56+
left: offset.dx,
57+
child: Material(
58+
color: Colors.transparent,
59+
child: LinkToPageMenu(
60+
editorState: editorState,
61+
),
62+
),
63+
);
64+
});
65+
66+
Overlay.of(context)?.insert(_linkToPageMenu!);
67+
68+
editorState.service.selectionService.currentSelection
69+
.addListener(_dismissLinkToPageMenu);
70+
}
71+
72+
class LinkToPageMenu extends StatefulWidget {
73+
final EditorState editorState;
74+
75+
const LinkToPageMenu({
76+
super.key,
77+
required this.editorState,
78+
});
79+
80+
@override
81+
State<LinkToPageMenu> createState() => _LinkToPageMenuState();
82+
}
83+
84+
class _LinkToPageMenuState extends State<LinkToPageMenu> {
85+
EditorStyle get style => widget.editorState.editorStyle;
86+
87+
@override
88+
Widget build(BuildContext context) {
89+
return Container(
90+
color: Colors.transparent,
91+
width: 300,
92+
child: Container(
93+
padding: const EdgeInsets.fromLTRB(10, 6, 10, 6),
94+
decoration: BoxDecoration(
95+
color: style.selectionMenuBackgroundColor,
96+
boxShadow: [
97+
BoxShadow(
98+
blurRadius: 5,
99+
spreadRadius: 1,
100+
color: Colors.black.withOpacity(0.1),
101+
),
102+
],
103+
borderRadius: BorderRadius.circular(6.0),
104+
),
105+
child: _buildBoardListWidget(context),
106+
),
107+
);
108+
}
109+
110+
Future<List<dartz.Tuple2<AppPB, List<ViewPB>>>> fetchBoards() async {
111+
return AppService().fetchViews(ViewLayoutTypePB.Board);
112+
}
113+
114+
Widget _buildBoardListWidget(BuildContext context) {
115+
return FutureBuilder<List<dartz.Tuple2<AppPB, List<ViewPB>>>>(
116+
builder: (context, snapshot) {
117+
if (snapshot.hasData &&
118+
snapshot.connectionState == ConnectionState.done) {
119+
final apps = snapshot.data;
120+
final children = <Widget>[
121+
Padding(
122+
padding: const EdgeInsets.symmetric(vertical: 4),
123+
child: FlowyText.regular(
124+
LocaleKeys.document_slashMenu_board_selectABoardToLinkTo.tr(),
125+
fontSize: 10,
126+
color: Colors.grey,
127+
),
128+
),
129+
];
130+
if (apps != null && apps.isNotEmpty) {
131+
for (final app in apps) {
132+
if (app.value2.isNotEmpty) {
133+
children.add(
134+
Padding(
135+
padding: const EdgeInsets.symmetric(vertical: 4),
136+
child: FlowyText.regular(
137+
app.value1.name,
138+
),
139+
),
140+
);
141+
for (final board in app.value2) {
142+
children.add(
143+
FlowyButton(
144+
leftIcon: svgWidget(
145+
'editor/board',
146+
color: Theme.of(context).colorScheme.onSurface,
147+
),
148+
text: FlowyText.regular(board.name),
149+
onTap: () => widget.editorState.insertBoard(
150+
app.value1,
151+
board,
152+
),
153+
),
154+
);
155+
}
156+
}
157+
}
158+
}
159+
return Column(
160+
crossAxisAlignment: CrossAxisAlignment.stretch,
161+
children: children,
162+
);
163+
} else {
164+
return const Center(
165+
child: CircularProgressIndicator(),
166+
);
167+
}
168+
},
169+
future: fetchBoards(),
170+
);
171+
}
172+
}
173+
174+
extension on EditorState {
175+
void insertBoard(AppPB appPB, ViewPB viewPB) {
176+
final selection = service.selectionService.currentSelection.value;
177+
final textNodes =
178+
service.selectionService.currentSelectedNodes.whereType<TextNode>();
179+
if (selection == null || textNodes.isEmpty) {
180+
return;
181+
}
182+
final transaction = this.transaction;
183+
transaction.insertNode(
184+
selection.end.path,
185+
Node(
186+
type: kBoardType,
187+
attributes: {
188+
kAppID: appPB.id,
189+
kBoardID: viewPB.id,
190+
},
191+
),
192+
);
193+
apply(transaction);
194+
}
195+
}

0 commit comments

Comments
 (0)