Просмотр исходного кода

Merge pull request #1102 from AppFlowy-IO/fix/ui_bugs

Fix/UI bugs
Nathan.fooo 2 лет назад
Родитель
Сommit
bb61bf0a30

+ 4 - 2
frontend/app_flowy/lib/plugins/grid/application/field/field_editor_bloc.dart

@@ -33,8 +33,10 @@ class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
             await dataController.loadTypeOptionData();
           },
           updateName: (name) {
-            dataController.fieldName = name;
-            emit(state.copyWith(name: name));
+            if (state.name != name) {
+              dataController.fieldName = name;
+              emit(state.copyWith(name: name));
+            }
           },
           didReceiveFieldChanged: (FieldPB field) {
             emit(state.copyWith(

+ 5 - 2
frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_cell.dart

@@ -58,7 +58,7 @@ class _DateCellState extends GridCellState<GridDateCell> {
   Widget build(BuildContext context) {
     final alignment = widget.cellStyle != null
         ? widget.cellStyle!.alignment
-        : Alignment.center;
+        : Alignment.centerLeft;
     return BlocProvider.value(
       value: _cellBloc,
       child: BlocBuilder<DateCellBloc, DateCellState>(
@@ -77,7 +77,10 @@ class _DateCellState extends GridCellState<GridDateCell> {
                   cursor: SystemMouseCursors.click,
                   child: Align(
                     alignment: alignment,
-                    child: FlowyText.medium(state.dateStr, fontSize: 12),
+                    child: FlowyText.medium(
+                      state.dateStr,
+                      fontSize: 12,
+                    ),
                   ),
                 ),
               ),

+ 41 - 21
frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/text_field.dart

@@ -11,20 +11,17 @@ import 'package:textfield_tags/textfield_tags.dart';
 
 import 'extension.dart';
 
-class SelectOptionTextField extends StatelessWidget {
-  final FocusNode _focusNode;
-  final TextEditingController _controller;
+class SelectOptionTextField extends StatefulWidget {
   final TextfieldTagsController tagController;
   final List<SelectOptionPB> options;
   final LinkedHashMap<String, SelectOptionPB> selectedOptionMap;
-
   final double distanceToText;
 
   final Function(String) onNewTag;
   final Function(String) newText;
   final VoidCallback? onClick;
 
-  SelectOptionTextField({
+  const SelectOptionTextField({
     required this.options,
     required this.selectedOptionMap,
     required this.distanceToText,
@@ -35,33 +32,55 @@ class SelectOptionTextField extends StatelessWidget {
     TextEditingController? textController,
     FocusNode? focusNode,
     Key? key,
-  })  : _controller = textController ?? TextEditingController(),
-        _focusNode = focusNode ?? FocusNode(),
-        super(key: key);
+  }) : super(key: key);
+
+  @override
+  State<SelectOptionTextField> createState() => _SelectOptionTextFieldState();
+}
+
+class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
+  late FocusNode focusNode;
+  late TextEditingController controller;
+
+  @override
+  void initState() {
+    focusNode = FocusNode();
+    controller = TextEditingController();
+
+    WidgetsBinding.instance.addPostFrameCallback((_) {
+      focusNode.requestFocus();
+    });
+    super.initState();
+  }
 
   @override
   Widget build(BuildContext context) {
     final theme = context.watch<AppTheme>();
 
     return TextFieldTags(
-      textEditingController: _controller,
-      textfieldTagsController: tagController,
-      initialTags: selectedOptionMap.keys.toList(),
-      focusNode: _focusNode,
+      textEditingController: controller,
+      textfieldTagsController: widget.tagController,
+      initialTags: widget.selectedOptionMap.keys.toList(),
+      focusNode: focusNode,
       textSeparators: const [','],
-      inputfieldBuilder: (BuildContext context, editController, focusNode,
-          error, onChanged, onSubmitted) {
+      inputfieldBuilder: (
+        BuildContext context,
+        editController,
+        focusNode,
+        error,
+        onChanged,
+        onSubmitted,
+      ) {
         return ((context, sc, tags, onTagDelegate) {
           return TextField(
-            autofocus: true,
             controller: editController,
             focusNode: focusNode,
-            onTap: onClick,
+            onTap: widget.onClick,
             onChanged: (text) {
               if (onChanged != null) {
                 onChanged(text);
               }
-              newText(text);
+              widget.newText(text);
             },
             onSubmitted: (text) {
               if (onSubmitted != null) {
@@ -69,7 +88,7 @@ class SelectOptionTextField extends StatelessWidget {
               }
 
               if (text.isNotEmpty) {
-                onNewTag(text);
+                widget.onNewTag(text);
                 focusNode.requestFocus();
               }
             },
@@ -83,7 +102,8 @@ class SelectOptionTextField extends StatelessWidget {
               isDense: true,
               prefixIcon: _renderTags(context, sc),
               hintText: LocaleKeys.grid_selectOption_searchOption.tr(),
-              prefixIconConstraints: BoxConstraints(maxWidth: distanceToText),
+              prefixIconConstraints:
+                  BoxConstraints(maxWidth: widget.distanceToText),
               focusedBorder: OutlineInputBorder(
                 borderSide: BorderSide(color: theme.main1, width: 1.0),
                 borderRadius: Corners.s10Border,
@@ -96,11 +116,11 @@ class SelectOptionTextField extends StatelessWidget {
   }
 
   Widget? _renderTags(BuildContext context, ScrollController sc) {
-    if (selectedOptionMap.isEmpty) {
+    if (widget.selectedOptionMap.isEmpty) {
       return null;
     }
 
-    final children = selectedOptionMap.values
+    final children = widget.selectedOptionMap.values
         .map((option) =>
             SelectOptionTag.fromOption(context: context, option: option))
         .toList();

+ 7 - 1
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell.dart

@@ -137,9 +137,11 @@ class _DragToExpandLine extends StatelessWidget {
 class FieldCellButton extends StatelessWidget {
   final VoidCallback onTap;
   final FieldPB field;
+  final int? maxLines;
   const FieldCellButton({
     required this.field,
     required this.onTap,
+    this.maxLines = 1,
     Key? key,
   }) : super(key: key);
 
@@ -150,7 +152,11 @@ class FieldCellButton extends StatelessWidget {
       hoverColor: theme.shader6,
       onTap: onTap,
       leftIcon: svgWidget(field.fieldType.iconName(), color: theme.iconColor),
-      text: FlowyText.medium(field.name, fontSize: 12),
+      text: FlowyText.medium(
+        field.name,
+        fontSize: 12,
+        maxLines: maxLines,
+      ),
       margin: GridSize.cellContentInsets,
     );
   }

+ 48 - 66
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_editor.dart

@@ -1,10 +1,9 @@
 import 'package:app_flowy/plugins/grid/application/field/field_editor_bloc.dart';
 import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
+import 'package:dartz/dartz.dart' show none;
 import 'package:easy_localization/easy_localization.dart';
-import 'package:app_flowy/workspace/presentation/widgets/dialogs.dart';
 import 'package:flowy_infra/theme.dart';
-import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flowy_infra_ui/style_widget/button.dart';
 import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flowy_infra_ui/widget/rounded_input_field.dart';
@@ -59,38 +58,38 @@ class _FieldEditorState extends State<FieldEditor> {
         isGroupField: widget.isGroupField,
         loader: widget.typeOptionLoader,
       )..add(const FieldEditorEvent.initial()),
-      child: BlocBuilder<FieldEditorBloc, FieldEditorState>(
-        builder: (context, state) {
-          return ListView(
-            shrinkWrap: true,
-            children: [
-              FlowyText.medium(
-                LocaleKeys.grid_field_editProperty.tr(),
-                fontSize: 12,
-              ),
-              const VSpace(10),
-              _FieldNameTextField(popoverMutex: popoverMutex),
-              const VSpace(10),
-              ..._addDeleteFieldButton(state),
-              _FieldTypeOptionCell(popoverMutex: popoverMutex),
-            ],
-          );
-        },
+      child: ListView(
+        shrinkWrap: true,
+        children: [
+          FlowyText.medium(
+            LocaleKeys.grid_field_editProperty.tr(),
+            fontSize: 12,
+          ),
+          const VSpace(10),
+          _FieldNameTextField(popoverMutex: popoverMutex),
+          const VSpace(10),
+          ..._addDeleteFieldButton(),
+          _FieldTypeOptionCell(popoverMutex: popoverMutex),
+        ],
       ),
     );
   }
 
-  List<Widget> _addDeleteFieldButton(FieldEditorState state) {
+  List<Widget> _addDeleteFieldButton() {
     if (widget.onDeleted == null) {
       return [];
     }
     return [
-      _DeleteFieldButton(
-        popoverMutex: popoverMutex,
-        onDeleted: () {
-          state.field.fold(
-            () => Log.error('Can not delete the field'),
-            (field) => widget.onDeleted?.call(field.id),
+      BlocBuilder<FieldEditorBloc, FieldEditorState>(
+        builder: (context, state) {
+          return _DeleteFieldButton(
+            popoverMutex: popoverMutex,
+            onDeleted: () {
+              state.field.fold(
+                () => Log.error('Can not delete the field'),
+                (field) => widget.onDeleted?.call(field.id),
+              );
+            },
           );
         },
       ),
@@ -139,13 +138,13 @@ class _FieldNameTextField extends StatefulWidget {
 }
 
 class _FieldNameTextFieldState extends State<_FieldNameTextField> {
-  late String name;
   FocusNode focusNode = FocusNode();
   VoidCallback? _popoverCallback;
-  TextEditingController controller = TextEditingController();
+  late TextEditingController controller;
 
   @override
   void initState() {
+    controller = TextEditingController();
     focusNode.addListener(() {
       if (focusNode.hasFocus) {
         widget.popoverMutex.close();
@@ -158,20 +157,29 @@ class _FieldNameTextFieldState extends State<_FieldNameTextField> {
   @override
   Widget build(BuildContext context) {
     final theme = context.watch<AppTheme>();
-
-    controller.text = context.read<FieldEditorBloc>().state.name;
-    return BlocListener<FieldEditorBloc, FieldEditorState>(
-      listenWhen: (previous, current) => previous.name != current.name,
-      listener: (context, state) {
-        controller.text = state.name;
-      },
+    return MultiBlocListener(
+      listeners: [
+        BlocListener<FieldEditorBloc, FieldEditorState>(
+          listenWhen: (p, c) => p.field == none(),
+          listener: (context, state) {
+            focusNode.requestFocus();
+          },
+        ),
+        BlocListener<FieldEditorBloc, FieldEditorState>(
+          listenWhen: (p, c) => controller.text != c.name,
+          listener: (context, state) {
+            controller.text = state.name;
+          },
+        ),
+      ],
       child: BlocBuilder<FieldEditorBloc, FieldEditorState>(
+        buildWhen: (previous, current) =>
+            previous.errorText != current.errorText,
         builder: (context, state) {
           listenOnPopoverChanged(context);
 
           return RoundedInputField(
             height: 36,
-            autoFocus: true,
             focusNode: focusNode,
             style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
             controller: controller,
@@ -193,23 +201,15 @@ class _FieldNameTextFieldState extends State<_FieldNameTextField> {
 
   void listenOnPopoverChanged(BuildContext context) {
     if (_popoverCallback != null) {
-      widget.popoverMutex.removePopoverStateListener(_popoverCallback!);
+      widget.popoverMutex.removePopoverListener(_popoverCallback!);
     }
-    _popoverCallback = widget.popoverMutex.listenOnPopoverStateChanged(() {
+    _popoverCallback = widget.popoverMutex.listenOnPopoverChanged(() {
       if (focusNode.hasFocus) {
         final node = FocusScope.of(context);
         node.unfocus();
       }
     });
   }
-
-  @override
-  void didUpdateWidget(covariant _FieldNameTextField oldWidget) {
-    controller.selection = TextSelection.fromPosition(
-        TextPosition(offset: controller.text.length));
-
-    super.didUpdateWidget(oldWidget);
-  }
 }
 
 class _DeleteFieldButton extends StatelessWidget {
@@ -235,29 +235,11 @@ class _DeleteFieldButton extends StatelessWidget {
             fontSize: 12,
             color: enable ? null : theme.shader4,
           ),
+          onTap: () => onDeleted?.call(),
         );
-        if (enable) button = _wrapPopover(button);
+        // if (enable) button = button;
         return button;
       },
     );
   }
-
-  Widget _wrapPopover(Widget widget) {
-    return AppFlowyPopover(
-      triggerActions: PopoverTriggerFlags.click,
-      constraints: BoxConstraints.loose(const Size(400, 240)),
-      mutex: popoverMutex,
-      direction: PopoverDirection.center,
-      popupBuilder: (popupContext) {
-        return PopoverAlertView(
-          title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
-          cancel: () {},
-          confirm: () {
-            onDeleted?.call();
-          },
-        );
-      },
-      child: widget,
-    );
-  }
 }

+ 1 - 1
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_type_option_editor.dart

@@ -65,7 +65,7 @@ class FieldTypeOptionEditor extends StatelessWidget {
     return SizedBox(
       height: GridSize.typeOptionItemHeight,
       child: AppFlowyPopover(
-        constraints: BoxConstraints.loose(const Size(460, 440)),
+        constraints: BoxConstraints.loose(const Size(460, 540)),
         asBarrier: true,
         triggerActions: PopoverTriggerFlags.click | PopoverTriggerFlags.hover,
         mutex: popoverMutex,

+ 2 - 0
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/date.dart

@@ -64,6 +64,7 @@ class DateTypeOptionWidget extends TypeOptionWidget {
   Widget _renderDateFormatButton(BuildContext context, DateFormat dataFormat) {
     return AppFlowyPopover(
       mutex: popoverMutex,
+      asBarrier: true,
       triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
       offset: const Offset(20, 0),
       constraints: BoxConstraints.loose(const Size(460, 440)),
@@ -85,6 +86,7 @@ class DateTypeOptionWidget extends TypeOptionWidget {
   Widget _renderTimeFormatButton(BuildContext context, TimeFormat timeFormat) {
     return AppFlowyPopover(
       mutex: popoverMutex,
+      asBarrier: true,
       triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
       offset: const Offset(20, 0),
       constraints: BoxConstraints.loose(const Size(460, 440)),

+ 94 - 75
frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart

@@ -2,6 +2,7 @@ import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_servic
 import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart';
 import 'package:app_flowy/plugins/grid/application/row/row_data_controller.dart';
 import 'package:app_flowy/plugins/grid/application/row/row_detail_bloc.dart';
+import 'package:app_flowy/workspace/presentation/widgets/dialogs.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra/theme.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -112,67 +113,61 @@ class _PropertyList extends StatelessWidget {
       builder: (context, state) {
         return Column(
           children: [
-            Expanded(
-              child: ScrollbarListStack(
-                axis: Axis.vertical,
-                controller: _scrollController,
-                barSize: GridSize.scrollBarSize,
-                autoHideScrollbar: false,
-                child: ListView.separated(
-                  controller: _scrollController,
-                  itemCount: state.gridCells.length,
-                  itemBuilder: (BuildContext context, int index) {
-                    return _RowDetailCell(
-                      cellId: state.gridCells[index],
-                      cellBuilder: cellBuilder,
-                    );
-                  },
-                  separatorBuilder: (BuildContext context, int index) {
-                    return const VSpace(2);
-                  },
-                ),
-              ),
-            ),
+            Expanded(child: _wrapScrollbar(buildList(state))),
             const VSpace(10),
             _CreateFieldButton(
               viewId: viewId,
-              onClosed: () {
-                WidgetsBinding.instance.addPostFrameCallback((_) {
-                  _scrollController.animateTo(
-                    _scrollController.position.maxScrollExtent,
-                    duration: const Duration(milliseconds: 250),
-                    curve: Curves.ease,
-                  );
-                });
-              },
-              onOpened: (controller) {
-                return FieldEditor(
-                  gridId: viewId,
-                  typeOptionLoader: NewFieldTypeOptionLoader(gridId: viewId),
-                  onDeleted: (fieldId) {
-                    controller.close();
-                    context
-                        .read<RowDetailBloc>()
-                        .add(RowDetailEvent.deleteField(fieldId));
-                  },
-                );
-              },
+              onClosed: _handleDidCreateField,
             ),
           ],
         );
       },
     );
   }
+
+  Widget buildList(RowDetailState state) {
+    return ListView.separated(
+      controller: _scrollController,
+      itemCount: state.gridCells.length,
+      itemBuilder: (BuildContext context, int index) {
+        return _RowDetailCell(
+          cellId: state.gridCells[index],
+          cellBuilder: cellBuilder,
+        );
+      },
+      separatorBuilder: (BuildContext context, int index) {
+        return const VSpace(2);
+      },
+    );
+  }
+
+  Widget _wrapScrollbar(Widget child) {
+    return ScrollbarListStack(
+      axis: Axis.vertical,
+      controller: _scrollController,
+      barSize: GridSize.scrollBarSize,
+      autoHideScrollbar: false,
+      child: child,
+    );
+  }
+
+  void _handleDidCreateField() {
+    WidgetsBinding.instance.addPostFrameCallback((_) {
+      _scrollController.animateTo(
+        _scrollController.position.maxScrollExtent,
+        duration: const Duration(milliseconds: 250),
+        curve: Curves.ease,
+      );
+    });
+  }
 }
 
 class _CreateFieldButton extends StatefulWidget {
   final String viewId;
-  final Widget Function(PopoverController) onOpened;
   final VoidCallback onClosed;
 
   const _CreateFieldButton({
     required this.viewId,
-    required this.onOpened,
     required this.onClosed,
     Key? key,
   }) : super(key: key);
@@ -213,8 +208,24 @@ class _CreateFieldButtonState extends State<_CreateFieldButton> {
           leftIcon: svgWidget("home/add"),
         ),
       ),
-      popupBuilder: (BuildContext context) =>
-          widget.onOpened(popoverController),
+      popupBuilder: (BuildContext popOverContext) {
+        return FieldEditor(
+          gridId: widget.viewId,
+          typeOptionLoader: NewFieldTypeOptionLoader(gridId: widget.viewId),
+          onDeleted: (fieldId) {
+            popoverController.close();
+
+            NavigatorAlertDialog(
+              title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
+              confirm: () {
+                context
+                    .read<RowDetailBloc>()
+                    .add(RowDetailEvent.deleteField(fieldId));
+              },
+            ).show(context);
+          },
+        );
+      },
     );
   }
 
@@ -260,41 +271,25 @@ class _RowDetailCellState extends State<_RowDetailCell> {
       ),
     );
 
-    return ConstrainedBox(
-      constraints: const BoxConstraints(minHeight: 40),
-      child: IntrinsicHeight(
+    return IntrinsicHeight(
+      child: ConstrainedBox(
+        constraints: const BoxConstraints(minHeight: 40),
         child: Row(
           crossAxisAlignment: CrossAxisAlignment.stretch,
           mainAxisAlignment: MainAxisAlignment.center,
           children: [
-            SizedBox(
-              width: 150,
-              child: Popover(
-                controller: popover,
-                offset: const Offset(20, 0),
-                popupBuilder: (popoverContext) {
-                  return OverlayContainer(
-                    constraints: BoxConstraints.loose(const Size(240, 600)),
-                    child: FieldEditor(
-                      gridId: widget.cellId.gridId,
-                      fieldName: widget.cellId.fieldContext.field.name,
-                      isGroupField: widget.cellId.fieldContext.isGroupField,
-                      typeOptionLoader: FieldTypeOptionLoader(
-                        gridId: widget.cellId.gridId,
-                        field: widget.cellId.fieldContext.field,
-                      ),
-                      onDeleted: (fieldId) {
-                        popover.close();
-                        context
-                            .read<RowDetailBloc>()
-                            .add(RowDetailEvent.deleteField(fieldId));
-                      },
-                    ),
-                  );
-                },
+            AppFlowyPopover(
+              controller: popover,
+              constraints: BoxConstraints.loose(const Size(240, 600)),
+              popupBuilder: (popoverContext) => buildFieldEditor(),
+              child: SizedBox(
+                width: 150,
                 child: FieldCellButton(
+                  maxLines: null,
                   field: widget.cellId.fieldContext.field,
-                  onTap: () => popover.show(),
+                  onTap: () {
+                    popover.show();
+                  },
                 ),
               ),
             ),
@@ -305,6 +300,30 @@ class _RowDetailCellState extends State<_RowDetailCell> {
       ),
     );
   }
+
+  Widget buildFieldEditor() {
+    return FieldEditor(
+      gridId: widget.cellId.gridId,
+      fieldName: widget.cellId.fieldContext.field.name,
+      isGroupField: widget.cellId.fieldContext.isGroupField,
+      typeOptionLoader: FieldTypeOptionLoader(
+        gridId: widget.cellId.gridId,
+        field: widget.cellId.fieldContext.field,
+      ),
+      onDeleted: (fieldId) {
+        popover.close();
+
+        NavigatorAlertDialog(
+          title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
+          confirm: () {
+            context
+                .read<RowDetailBloc>()
+                .add(RowDetailEvent.deleteField(fieldId));
+          },
+        ).show(context);
+      },
+    );
+  }
 }
 
 GridCellStyle? _customCellStyle(AppTheme theme, FieldType fieldType) {

+ 1 - 1
frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_property.dart

@@ -120,7 +120,7 @@ class _GridPropertyCell extends StatelessWidget {
       mutex: popoverMutex,
       triggerActions: PopoverTriggerFlags.click,
       offset: const Offset(20, 0),
-      constraints: BoxConstraints.loose(const Size(240, 200)),
+      constraints: BoxConstraints.loose(const Size(240, 400)),
       child: FlowyButton(
         text: FlowyText.medium(fieldContext.name, fontSize: 12),
         hoverColor: theme.hover,

+ 6 - 6
frontend/app_flowy/lib/plugins/trash/trash.dart

@@ -140,23 +140,23 @@ class _TrashPageState extends State<TrashPage> {
     return SizedBox(
       height: 36,
       child: Row(
+        crossAxisAlignment: CrossAxisAlignment.stretch,
         children: [
           FlowyText.semibold(LocaleKeys.trash_text.tr()),
           const Spacer(),
-          SizedBox.fromSize(
-            size: const Size(102, 30),
+          IntrinsicWidth(
             child: FlowyButton(
               text: FlowyText.medium(LocaleKeys.trash_restoreAll.tr(),
                   fontSize: 12),
               leftIcon: svgWidget('editor/restore', color: theme.iconColor),
               hoverColor: theme.hover,
-              onTap: () =>
-                  context.read<TrashBloc>().add(const TrashEvent.restoreAll()),
+              onTap: () => context.read<TrashBloc>().add(
+                    const TrashEvent.restoreAll(),
+                  ),
             ),
           ),
           const HSpace(6),
-          SizedBox.fromSize(
-            size: const Size(102, 30),
+          IntrinsicWidth(
             child: FlowyButton(
               text: FlowyText.medium(LocaleKeys.trash_deleteAll.tr(),
                   fontSize: 12),

+ 0 - 36
frontend/app_flowy/lib/workspace/presentation/widgets/dialogs.dart

@@ -86,42 +86,6 @@ class _CreateTextFieldDialog extends State<NavigatorTextFieldDialog> {
   }
 }
 
-class PopoverAlertView extends StatelessWidget {
-  final String title;
-  final void Function()? cancel;
-  final void Function()? confirm;
-
-  const PopoverAlertView({
-    required this.title,
-    this.confirm,
-    this.cancel,
-    Key? key,
-  }) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    final theme = context.watch<AppTheme>();
-    return StyledDialog(
-      child: Column(
-        crossAxisAlignment: CrossAxisAlignment.start,
-        mainAxisAlignment: MainAxisAlignment.center,
-        children: <Widget>[
-          ...[
-            FlowyText.medium(title, color: theme.shader4),
-          ],
-          if (confirm != null) ...[
-            const VSpace(20),
-            OkCancelButton(
-              onOkPressed: confirm,
-              onCancelPressed: cancel,
-            )
-          ]
-        ],
-      ),
-    );
-  }
-}
-
 class NavigatorAlertDialog extends StatefulWidget {
   final String title;
   final void Function()? cancel;

+ 21 - 14
frontend/app_flowy/packages/appflowy_popover/lib/src/mutex.dart

@@ -5,14 +5,14 @@ import 'popover.dart';
 /// If multiple popovers are exclusive,
 /// pass the same mutex to them.
 class PopoverMutex {
-  final ValueNotifier<PopoverState?> _stateNotifier = ValueNotifier(null);
+  final _PopoverStateNotifier _stateNotifier = _PopoverStateNotifier();
   PopoverMutex();
 
-  void removePopoverStateListener(VoidCallback listener) {
+  void removePopoverListener(VoidCallback listener) {
     _stateNotifier.removeListener(listener);
   }
 
-  VoidCallback listenOnPopoverStateChanged(VoidCallback callback) {
+  VoidCallback listenOnPopoverChanged(VoidCallback callback) {
     listenerCallback() {
       callback();
     }
@@ -21,24 +21,31 @@ class PopoverMutex {
     return listenerCallback;
   }
 
-  void close() {
-    _stateNotifier.value?.close();
-  }
+  void close() => _stateNotifier.state?.close();
 
-  PopoverState? get state => _stateNotifier.value;
+  PopoverState? get state => _stateNotifier.state;
 
-  set state(PopoverState? newState) {
-    if (_stateNotifier.value != null && _stateNotifier.value != newState) {
-      _stateNotifier.value?.close();
-    }
-    _stateNotifier.value = newState;
-  }
+  set state(PopoverState? newState) => _stateNotifier.state = newState;
 
   void removeState() {
-    _stateNotifier.value = null;
+    _stateNotifier.state = null;
   }
 
   void dispose() {
     _stateNotifier.dispose();
   }
 }
+
+class _PopoverStateNotifier extends ChangeNotifier {
+  PopoverState? _state;
+
+  PopoverState? get state => _state;
+
+  set state(PopoverState? newState) {
+    if (_state != null && _state != newState) {
+      _state?.close();
+    }
+    _state = newState;
+    notifyListeners();
+  }
+}

+ 30 - 9
frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/text.dart

@@ -8,6 +8,7 @@ class FlowyText extends StatelessWidget {
   final double fontSize;
   final FontWeight fontWeight;
   final TextAlign? textAlign;
+  final int? maxLines;
   final Color? color;
 
   const FlowyText(
@@ -18,21 +19,40 @@ class FlowyText extends StatelessWidget {
     this.fontWeight = FontWeight.w400,
     this.textAlign,
     this.color,
+    this.maxLines = 1,
   }) : super(key: key);
 
-  const FlowyText.semibold(this.title,
-      {Key? key, this.fontSize = 16, this.overflow, this.color, this.textAlign})
-      : fontWeight = FontWeight.w600,
+  const FlowyText.semibold(
+    this.title, {
+    Key? key,
+    this.fontSize = 16,
+    this.overflow,
+    this.color,
+    this.textAlign,
+    this.maxLines = 1,
+  })  : fontWeight = FontWeight.w600,
         super(key: key);
 
-  const FlowyText.medium(this.title,
-      {Key? key, this.fontSize = 16, this.overflow, this.color, this.textAlign})
-      : fontWeight = FontWeight.w500,
+  const FlowyText.medium(
+    this.title, {
+    Key? key,
+    this.fontSize = 16,
+    this.overflow,
+    this.color,
+    this.textAlign,
+    this.maxLines = 1,
+  })  : fontWeight = FontWeight.w500,
         super(key: key);
 
-  const FlowyText.regular(this.title,
-      {Key? key, this.fontSize = 16, this.overflow, this.color, this.textAlign})
-      : fontWeight = FontWeight.w400,
+  const FlowyText.regular(
+    this.title, {
+    Key? key,
+    this.fontSize = 16,
+    this.overflow,
+    this.color,
+    this.textAlign,
+    this.maxLines = 1,
+  })  : fontWeight = FontWeight.w400,
         super(key: key);
 
   @override
@@ -40,6 +60,7 @@ class FlowyText extends StatelessWidget {
     final theme = context.watch<AppTheme>();
     return Text(
       title,
+      maxLines: maxLines,
       textAlign: textAlign,
       overflow: overflow ?? TextOverflow.clip,
       style: TextStyle(