mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-11-08 06:29:07 +00:00
fix: hide continue writing when document is empty (#7498)
* fix: hide continue writing when document is empty * chore: code clean up and add documentation
This commit is contained in:
parent
070cde9ecb
commit
b59eba76a6
@ -9,6 +9,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/shared_con
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/transaction_handler/editor_transaction_service.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
||||
import 'package:appflowy/shared/flowy_error_page.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view_info/view_info_bloc.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
@ -71,9 +72,16 @@ class _RowEditor extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (_) =>
|
||||
DocumentBloc(documentId: view.id)..add(const DocumentEvent.initial()),
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (_) => DocumentBloc(documentId: view.id)
|
||||
..add(const DocumentEvent.initial()),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (_) => ViewBloc(view: view)..add(const ViewEvent.initial()),
|
||||
),
|
||||
],
|
||||
child: BlocConsumer<DocumentBloc, DocumentState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.isDocumentEmpty != current.isDocumentEmpty,
|
||||
|
||||
@ -21,6 +21,8 @@ import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../workspace/application/view/view_bloc.dart';
|
||||
|
||||
// This widget is largely copied from `plugins/document/document_page.dart` intentionally instead of opting for an abstraction. We can make an abstraction after the view refactor is done and there's more clarity in that department.
|
||||
|
||||
class DatabaseDocumentPage extends StatefulWidget {
|
||||
@ -72,6 +74,10 @@ class _DatabaseDocumentPageState extends State<DatabaseDocumentPage> {
|
||||
documentId: widget.documentId,
|
||||
)..add(const DocumentEvent.initial()),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (_) =>
|
||||
ViewBloc(view: widget.view)..add(const ViewEvent.initial()),
|
||||
),
|
||||
],
|
||||
child: BlocBuilder<DocumentBloc, DocumentState>(
|
||||
builder: (context, state) {
|
||||
|
||||
@ -92,6 +92,10 @@ class _DocumentPageState extends State<DocumentPage>
|
||||
ViewLockStatusEvent.initial(),
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (_) =>
|
||||
ViewBloc(view: widget.view)..add(const ViewEvent.initial()),
|
||||
),
|
||||
],
|
||||
child: BlocConsumer<ViewLockStatusBloc, ViewLockStatusState>(
|
||||
listenWhen: (prev, curr) => curr.isLocked != prev.isLocked,
|
||||
|
||||
@ -15,6 +15,7 @@ import 'package:appflowy/plugins/inline_actions/inline_actions_service.dart';
|
||||
import 'package:appflowy/shared/feature_flags.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||
import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view_info/view_info_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/af_focus_manager.dart';
|
||||
@ -437,11 +438,13 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage>
|
||||
}) {
|
||||
final documentBloc = context.read<DocumentBloc>();
|
||||
final isLocalMode = documentBloc.isLocalMode;
|
||||
final view = context.read<ViewBloc>().state.view;
|
||||
return slashMenuItemsBuilder(
|
||||
editorState: editorState,
|
||||
node: node,
|
||||
isLocalMode: isLocalMode,
|
||||
documentBloc: documentBloc,
|
||||
view: view,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/presentation/message/ai_markdown_text.dart';
|
||||
import 'package:appflowy/plugins/document/application/prelude.dart';
|
||||
import 'package:appflowy/util/theme_extension.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
@ -187,6 +188,7 @@ class _AIWriterBlockComponentState extends State<AiWriterBlockComponent> {
|
||||
),
|
||||
width: constraints.maxWidth,
|
||||
child: OverlayContent(
|
||||
editorState: editorState,
|
||||
node: widget.node,
|
||||
),
|
||||
),
|
||||
@ -236,9 +238,11 @@ class _AIWriterBlockComponentState extends State<AiWriterBlockComponent> {
|
||||
class OverlayContent extends StatelessWidget {
|
||||
const OverlayContent({
|
||||
super.key,
|
||||
required this.editorState,
|
||||
required this.node,
|
||||
});
|
||||
|
||||
final EditorState editorState;
|
||||
final Node node;
|
||||
|
||||
@override
|
||||
@ -372,28 +376,12 @@ class OverlayContent extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (showActionPopup) ...[
|
||||
const VSpace(4.0 + 1.0),
|
||||
Container(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
constraints: BoxConstraints(minWidth: 240.0),
|
||||
decoration: _getModalDecoration(
|
||||
..._bottomActions(
|
||||
context,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderColor: lightBorderColor,
|
||||
borderRadius: BorderRadius.all(Radius.circular(8.0)),
|
||||
showActionPopup,
|
||||
hasSelection,
|
||||
lightBorderColor,
|
||||
),
|
||||
child: IntrinsicWidth(
|
||||
child: SeparatedColumn(
|
||||
separatorBuilder: () => const VSpace(4.0),
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _getCommands(
|
||||
hasSelection: hasSelection,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
@ -477,6 +465,54 @@ class OverlayContent extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _bottomActions(
|
||||
BuildContext context,
|
||||
bool showActionPopup,
|
||||
bool hasSelection,
|
||||
Color borderColor,
|
||||
) {
|
||||
if (!showActionPopup) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (editorState.isEmptyForContinueWriting()) {
|
||||
final documentContext = editorState.document.root.context;
|
||||
if (documentContext == null) {
|
||||
return [];
|
||||
}
|
||||
final view = documentContext.read<ViewBloc>().state.view;
|
||||
if (view.name.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
// add one here to take into account the border of the main message box.
|
||||
// It is configured to be on the outside to hide some graphical
|
||||
// artifacts.
|
||||
const VSpace(4.0 + 1.0),
|
||||
Container(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
constraints: BoxConstraints(minWidth: 240.0),
|
||||
decoration: _getModalDecoration(
|
||||
context,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderColor: borderColor,
|
||||
borderRadius: BorderRadius.all(Radius.circular(8.0)),
|
||||
),
|
||||
child: IntrinsicWidth(
|
||||
child: SeparatedColumn(
|
||||
separatorBuilder: () => const VSpace(4.0),
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _getCommands(
|
||||
hasSelection: hasSelection,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
List<Widget> _getCommands({required bool hasSelection}) {
|
||||
if (hasSelection) {
|
||||
return [
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/document_markdown_parsers.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/sub_page_node_parser.dart';
|
||||
import 'package:appflowy/shared/markdown_to_document.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
|
||||
@ -87,4 +89,64 @@ extension AiWriterNodeExtension on EditorState {
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/// Determines whether the document is empty up to the selection
|
||||
///
|
||||
/// If empty and the title is also empty, the continue writing option will be disabled.
|
||||
bool isEmptyForContinueWriting({
|
||||
Selection? selection,
|
||||
}) {
|
||||
if (selection != null && !selection.isCollapsed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final effectiveSelection = Selection(
|
||||
start: Position(path: [0]),
|
||||
end: selection?.normalized.end ??
|
||||
this.selection?.normalized.end ??
|
||||
Position(path: [0]),
|
||||
);
|
||||
|
||||
// if the selected nodes are not entirely selected, slice the nodes
|
||||
final slicedNodes = <Node>[];
|
||||
final nodes = getNodesInSelection(effectiveSelection);
|
||||
|
||||
for (final node in nodes) {
|
||||
final delta = node.delta;
|
||||
if (delta == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final slicedDelta = delta.slice(
|
||||
node == nodes.first ? effectiveSelection.startIndex : 0,
|
||||
node == nodes.last ? effectiveSelection.endIndex : delta.length,
|
||||
);
|
||||
|
||||
final copiedNode = node.copyWith(
|
||||
attributes: {
|
||||
...node.attributes,
|
||||
blockComponentDelta: slicedDelta.toJson(),
|
||||
},
|
||||
);
|
||||
|
||||
slicedNodes.add(copiedNode);
|
||||
}
|
||||
|
||||
// using less custom parsers to avoid futures
|
||||
final markdown = documentToMarkdown(
|
||||
Document.blank()..insert([0], slicedNodes),
|
||||
customParsers: [
|
||||
const MathEquationNodeParser(),
|
||||
const CalloutNodeParser(),
|
||||
const ToggleListNodeParser(),
|
||||
const CustomParagraphNodeParser(),
|
||||
const SubPageNodeParser(),
|
||||
const SimpleTableNodeParser(),
|
||||
const LinkPreviewNodeParser(),
|
||||
const FileBlockNodeParser(),
|
||||
],
|
||||
);
|
||||
|
||||
return markdown.trim().isEmpty;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import 'package:appflowy/plugins/document/application/document_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_node_extension.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
@ -13,9 +15,16 @@ List<SelectionMenuItem> slashMenuItemsBuilder({
|
||||
DocumentBloc? documentBloc,
|
||||
EditorState? editorState,
|
||||
Node? node,
|
||||
ViewPB? view,
|
||||
}) {
|
||||
final isInTable = node != null && node.parentTableCellNode != null;
|
||||
final isMobile = UniversalPlatform.isMobile;
|
||||
bool isEmpty = false;
|
||||
if (editorState == null || editorState.isEmptyForContinueWriting()) {
|
||||
if (view == null || view.name.isEmpty) {
|
||||
isEmpty = true;
|
||||
}
|
||||
}
|
||||
if (isMobile) {
|
||||
if (isInTable) {
|
||||
return mobileItemsInTale;
|
||||
@ -29,6 +38,7 @@ List<SelectionMenuItem> slashMenuItemsBuilder({
|
||||
return _defaultSlashMenuItems(
|
||||
isLocalMode: isLocalMode,
|
||||
documentBloc: documentBloc,
|
||||
isEmpty: isEmpty,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -48,11 +58,12 @@ List<SelectionMenuItem> slashMenuItemsBuilder({
|
||||
List<SelectionMenuItem> _defaultSlashMenuItems({
|
||||
bool isLocalMode = false,
|
||||
DocumentBloc? documentBloc,
|
||||
bool isEmpty = false,
|
||||
}) {
|
||||
return [
|
||||
// ai
|
||||
if (!isLocalMode) ...[
|
||||
continueWritingSlashMenuItem,
|
||||
if (!isEmpty) continueWritingSlashMenuItem,
|
||||
aiWriterSlashMenuItem,
|
||||
],
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user