mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-11-07 05:38:51 +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_plugins/transaction_handler/editor_transaction_service.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
||||||
import 'package:appflowy/shared/flowy_error_page.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/workspace/application/view_info/view_info_bloc.dart';
|
||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
@ -71,9 +72,16 @@ class _RowEditor extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider(
|
return MultiBlocProvider(
|
||||||
create: (_) =>
|
providers: [
|
||||||
DocumentBloc(documentId: view.id)..add(const DocumentEvent.initial()),
|
BlocProvider(
|
||||||
|
create: (_) => DocumentBloc(documentId: view.id)
|
||||||
|
..add(const DocumentEvent.initial()),
|
||||||
|
),
|
||||||
|
BlocProvider(
|
||||||
|
create: (_) => ViewBloc(view: view)..add(const ViewEvent.initial()),
|
||||||
|
),
|
||||||
|
],
|
||||||
child: BlocConsumer<DocumentBloc, DocumentState>(
|
child: BlocConsumer<DocumentBloc, DocumentState>(
|
||||||
listenWhen: (previous, current) =>
|
listenWhen: (previous, current) =>
|
||||||
previous.isDocumentEmpty != current.isDocumentEmpty,
|
previous.isDocumentEmpty != current.isDocumentEmpty,
|
||||||
|
|||||||
@ -21,6 +21,8 @@ import 'package:appflowy_editor/appflowy_editor.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.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.
|
// 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 {
|
class DatabaseDocumentPage extends StatefulWidget {
|
||||||
@ -72,6 +74,10 @@ class _DatabaseDocumentPageState extends State<DatabaseDocumentPage> {
|
|||||||
documentId: widget.documentId,
|
documentId: widget.documentId,
|
||||||
)..add(const DocumentEvent.initial()),
|
)..add(const DocumentEvent.initial()),
|
||||||
),
|
),
|
||||||
|
BlocProvider(
|
||||||
|
create: (_) =>
|
||||||
|
ViewBloc(view: widget.view)..add(const ViewEvent.initial()),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
child: BlocBuilder<DocumentBloc, DocumentState>(
|
child: BlocBuilder<DocumentBloc, DocumentState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
|
|||||||
@ -92,6 +92,10 @@ class _DocumentPageState extends State<DocumentPage>
|
|||||||
ViewLockStatusEvent.initial(),
|
ViewLockStatusEvent.initial(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
BlocProvider(
|
||||||
|
create: (_) =>
|
||||||
|
ViewBloc(view: widget.view)..add(const ViewEvent.initial()),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
child: BlocConsumer<ViewLockStatusBloc, ViewLockStatusState>(
|
child: BlocConsumer<ViewLockStatusBloc, ViewLockStatusState>(
|
||||||
listenWhen: (prev, curr) => curr.isLocked != prev.isLocked,
|
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/shared/feature_flags.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.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/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/view_lock_status_bloc.dart';
|
||||||
import 'package:appflowy/workspace/application/view_info/view_info_bloc.dart';
|
import 'package:appflowy/workspace/application/view_info/view_info_bloc.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/af_focus_manager.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 documentBloc = context.read<DocumentBloc>();
|
||||||
final isLocalMode = documentBloc.isLocalMode;
|
final isLocalMode = documentBloc.isLocalMode;
|
||||||
|
final view = context.read<ViewBloc>().state.view;
|
||||||
return slashMenuItemsBuilder(
|
return slashMenuItemsBuilder(
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
node: node,
|
node: node,
|
||||||
isLocalMode: isLocalMode,
|
isLocalMode: isLocalMode,
|
||||||
documentBloc: documentBloc,
|
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/ai_chat/presentation/message/ai_markdown_text.dart';
|
||||||
import 'package:appflowy/plugins/document/application/prelude.dart';
|
import 'package:appflowy/plugins/document/application/prelude.dart';
|
||||||
import 'package:appflowy/util/theme_extension.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/home/menu/sidebar/space/shared_widget.dart';
|
||||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
@ -187,6 +188,7 @@ class _AIWriterBlockComponentState extends State<AiWriterBlockComponent> {
|
|||||||
),
|
),
|
||||||
width: constraints.maxWidth,
|
width: constraints.maxWidth,
|
||||||
child: OverlayContent(
|
child: OverlayContent(
|
||||||
|
editorState: editorState,
|
||||||
node: widget.node,
|
node: widget.node,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -236,9 +238,11 @@ class _AIWriterBlockComponentState extends State<AiWriterBlockComponent> {
|
|||||||
class OverlayContent extends StatelessWidget {
|
class OverlayContent extends StatelessWidget {
|
||||||
const OverlayContent({
|
const OverlayContent({
|
||||||
super.key,
|
super.key,
|
||||||
|
required this.editorState,
|
||||||
required this.node,
|
required this.node,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final EditorState editorState;
|
||||||
final Node node;
|
final Node node;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -372,28 +376,12 @@ class OverlayContent extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (showActionPopup) ...[
|
..._bottomActions(
|
||||||
const VSpace(4.0 + 1.0),
|
context,
|
||||||
Container(
|
showActionPopup,
|
||||||
padding: EdgeInsets.all(8.0),
|
hasSelection,
|
||||||
constraints: BoxConstraints(minWidth: 240.0),
|
lightBorderColor,
|
||||||
decoration: _getModalDecoration(
|
),
|
||||||
context,
|
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
borderColor: lightBorderColor,
|
|
||||||
borderRadius: BorderRadius.all(Radius.circular(8.0)),
|
|
||||||
),
|
|
||||||
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}) {
|
List<Widget> _getCommands({required bool hasSelection}) {
|
||||||
if (hasSelection) {
|
if (hasSelection) {
|
||||||
return [
|
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/shared/markdown_to_document.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
|
||||||
@ -87,4 +89,64 @@ extension AiWriterNodeExtension on EditorState {
|
|||||||
|
|
||||||
return res;
|
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/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/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:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:universal_platform/universal_platform.dart';
|
import 'package:universal_platform/universal_platform.dart';
|
||||||
|
|
||||||
@ -13,9 +15,16 @@ List<SelectionMenuItem> slashMenuItemsBuilder({
|
|||||||
DocumentBloc? documentBloc,
|
DocumentBloc? documentBloc,
|
||||||
EditorState? editorState,
|
EditorState? editorState,
|
||||||
Node? node,
|
Node? node,
|
||||||
|
ViewPB? view,
|
||||||
}) {
|
}) {
|
||||||
final isInTable = node != null && node.parentTableCellNode != null;
|
final isInTable = node != null && node.parentTableCellNode != null;
|
||||||
final isMobile = UniversalPlatform.isMobile;
|
final isMobile = UniversalPlatform.isMobile;
|
||||||
|
bool isEmpty = false;
|
||||||
|
if (editorState == null || editorState.isEmptyForContinueWriting()) {
|
||||||
|
if (view == null || view.name.isEmpty) {
|
||||||
|
isEmpty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
if (isInTable) {
|
if (isInTable) {
|
||||||
return mobileItemsInTale;
|
return mobileItemsInTale;
|
||||||
@ -29,6 +38,7 @@ List<SelectionMenuItem> slashMenuItemsBuilder({
|
|||||||
return _defaultSlashMenuItems(
|
return _defaultSlashMenuItems(
|
||||||
isLocalMode: isLocalMode,
|
isLocalMode: isLocalMode,
|
||||||
documentBloc: documentBloc,
|
documentBloc: documentBloc,
|
||||||
|
isEmpty: isEmpty,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -48,11 +58,12 @@ List<SelectionMenuItem> slashMenuItemsBuilder({
|
|||||||
List<SelectionMenuItem> _defaultSlashMenuItems({
|
List<SelectionMenuItem> _defaultSlashMenuItems({
|
||||||
bool isLocalMode = false,
|
bool isLocalMode = false,
|
||||||
DocumentBloc? documentBloc,
|
DocumentBloc? documentBloc,
|
||||||
|
bool isEmpty = false,
|
||||||
}) {
|
}) {
|
||||||
return [
|
return [
|
||||||
// ai
|
// ai
|
||||||
if (!isLocalMode) ...[
|
if (!isLocalMode) ...[
|
||||||
continueWritingSlashMenuItem,
|
if (!isEmpty) continueWritingSlashMenuItem,
|
||||||
aiWriterSlashMenuItem,
|
aiWriterSlashMenuItem,
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user