fix: 0.3.8 known issues (#3912)

* fix: add a left padding to align the document and grid field

* fix: emoji picker in the slash menu is too small

* fix: replace the delete icon color with black

* fix: improve snackbar background color

* fix: cannot add new line after toggle list

* feat: set  as the default icon of getting started

* fix: the titlebar overflows when the title level is too deep

* fix: integration test

* fix: openAI hint text overflow

* fix: integration tests
This commit is contained in:
Lucas.Xu 2023-11-13 12:00:03 +08:00 committed by GitHub
parent 7cee8e392f
commit 251c6d22b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 180 additions and 109 deletions

View File

@ -22,7 +22,6 @@ void main() {
// Hover over cover toolbar to show 'Add Cover' and 'Add Icon' buttons
await tester.editor.hoverOnCoverToolbar();
tester.expectToSeePluginAddCoverAndIconButton();
// Insert a document cover
await tester.editor.tapOnAddCover();
@ -58,14 +57,10 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapGoButton();
tester.expectToSeeDocumentIcon(null);
// Hover over cover toolbar to show the 'Add Cover' and 'Add Icon' buttons
await tester.editor.hoverOnCoverToolbar();
tester.expectToSeePluginAddCoverAndIconButton();
tester.expectToSeeDocumentIcon('⭐️');
// Insert a document icon
await tester.editor.tapAddIconButton();
await tester.editor.tapGettingStartedIcon();
await tester.tapEmoji('😀');
tester.expectToSeeDocumentIcon('😀');
@ -95,18 +90,15 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapGoButton();
tester.expectToSeeDocumentIcon(null);
tester.expectToSeeDocumentIcon('⭐️');
tester.expectToSeeNoDocumentCover();
// Hover over cover toolbar to show the 'Add Cover' and 'Add Icon' buttons
await tester.editor.hoverOnCoverToolbar();
tester.expectToSeePluginAddCoverAndIconButton();
// Insert a document icon
await tester.editor.tapAddIconButton();
await tester.editor.tapGettingStartedIcon();
await tester.tapEmoji('😀');
// Insert a document cover
await tester.editor.hoverOnCoverToolbar();
await tester.editor.tapOnAddCover();
// Expect to see the icon and cover at the same time
@ -122,8 +114,7 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.editor.hoverOnCoverToolbar();
await tester.editor.tapAddIconButton();
await tester.editor.tapGettingStartedIcon();
// click the shuffle button
await tester.tapButton(
@ -136,8 +127,7 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.editor.hoverOnCoverToolbar();
await tester.editor.tapAddIconButton();
await tester.editor.tapGettingStartedIcon();
final searchEmojiTextField = find.byWidgetPredicate(
(widget) =>

View File

@ -60,6 +60,15 @@ class EditorOperations {
expect(find.byType(FlowyEmojiPicker), findsOneWidget);
}
Future<void> tapGettingStartedIcon() async {
await tester.tapButton(
find.descendant(
of: find.byType(DocumentHeaderNodeWidget),
matching: find.findTextInFlowyText('⭐️'),
),
);
}
/// Taps on the 'Skin tone' button
///
/// Must call [tapAddIconButton] first.
@ -67,7 +76,7 @@ class EditorOperations {
await tester.tapButton(
find.byTooltip(LocaleKeys.emoji_selectSkinTone.tr()),
);
final skinToneButton = find.text(EmojiSkinToneWrapper(skinTone).name);
final skinToneButton = find.byKey(emojiSkinToneKey(skinTone.icon));
await tester.tapButton(skinToneButton);
}

View File

@ -12,7 +12,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter_test/flutter_test.dart';
// const String readme = 'Read me';
const String gettingStarted = '⭐️ Getting started';
const String gettingStarted = 'Getting started';
extension Expectation on WidgetTester {
/// Expect to see the home page and with a default read me page.

View File

@ -24,7 +24,6 @@ class BottomSheetActionWidget extends StatelessWidget {
icon: FlowySvg(
svg,
size: const Size.square(22.0),
blendMode: BlendMode.dst,
color: iconColor,
),
label: Text(text),

View File

@ -1,5 +1,4 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:emoji_mart/emoji_mart.dart';
@ -10,6 +9,11 @@ import 'package:flutter/material.dart';
// use a temporary global value to store last selected skin tone
EmojiSkinTone? lastSelectedEmojiSkinTone;
@visibleForTesting
ValueKey emojiSkinToneKey(String icon) {
return ValueKey('emoji_skin_tone_$icon');
}
class FlowyEmojiSkinToneSelector extends StatefulWidget {
const FlowyEmojiSkinToneSelector({
super.key,
@ -26,73 +30,59 @@ class FlowyEmojiSkinToneSelector extends StatefulWidget {
class _FlowyEmojiSkinToneSelectorState
extends State<FlowyEmojiSkinToneSelector> {
EmojiSkinTone skinTone = EmojiSkinTone.none;
final controller = PopoverController();
@override
Widget build(BuildContext context) {
return PopoverActionList<EmojiSkinToneWrapper>(
return AppFlowyPopover(
direction: PopoverDirection.bottomWithCenterAligned,
offset: const Offset(0, 8),
actions: EmojiSkinTone.values
.map((action) => EmojiSkinToneWrapper(action))
.toList(),
buildChild: (controller) {
return FlowyTooltip(
message: LocaleKeys.emoji_selectSkinTone.tr(),
child: FlowyIconButton(
icon: Padding(
// add a left padding to align the emoji center
padding: const EdgeInsets.only(
left: 3.0,
),
child: FlowyText(
lastSelectedEmojiSkinTone?.icon ?? '',
fontSize: 22.0,
),
),
onPressed: () => controller.show(),
),
controller: controller,
popupBuilder: (context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: EmojiSkinTone.values
.map(
(e) => _buildIconButton(
e.icon,
() {
setState(() => lastSelectedEmojiSkinTone = e);
widget.onEmojiSkinToneChanged(e);
controller.close();
},
),
)
.toList(),
);
},
onSelected: (action, controller) async {
widget.onEmojiSkinToneChanged(action.inner);
setState(() {
lastSelectedEmojiSkinTone = action.inner;
});
controller.close();
},
child: FlowyTooltip(
message: LocaleKeys.emoji_selectSkinTone.tr(),
child: _buildIconButton(
lastSelectedEmojiSkinTone?.icon ?? '',
() => controller.show(),
),
),
);
}
Widget _buildIconButton(String icon, VoidCallback onPressed) {
return FlowyIconButton(
key: emojiSkinToneKey(icon),
icon: Padding(
// add a left padding to align the emoji center
padding: const EdgeInsets.only(
left: 3.0,
),
child: FlowyText(
icon,
fontSize: 22.0,
),
),
onPressed: onPressed,
);
}
}
class EmojiSkinToneWrapper extends ActionCell {
EmojiSkinToneWrapper(this.inner);
final EmojiSkinTone inner;
Widget? icon(Color iconColor) => null;
@override
String get name {
final String i18n;
switch (inner) {
case EmojiSkinTone.none:
i18n = LocaleKeys.emoji_skinTone_default.tr();
case EmojiSkinTone.light:
i18n = LocaleKeys.emoji_skinTone_light.tr();
case EmojiSkinTone.mediumLight:
i18n = LocaleKeys.emoji_skinTone_mediumLight.tr();
case EmojiSkinTone.medium:
i18n = LocaleKeys.emoji_skinTone_medium.tr();
case EmojiSkinTone.mediumDark:
i18n = LocaleKeys.emoji_skinTone_mediumDark.tr();
case EmojiSkinTone.dark:
i18n = LocaleKeys.emoji_skinTone_dark.tr();
}
return '${inner.icon} $i18n';
}
}
extension on EmojiSkinTone {
extension EmojiSkinToneIcon on EmojiSkinTone {
String get icon {
switch (this) {
case EmojiSkinTone.none:

View File

@ -123,7 +123,7 @@ class _RowEditorState extends State<RowEditor> {
scrollController: widget.scrollController,
styleCustomizer: EditorStyleCustomizer(
context: context,
padding: const EdgeInsets.symmetric(horizontal: 10),
padding: const EdgeInsets.only(left: 16, right: 54),
),
showParagraphPlaceholder: (editorState, node) =>
editorState.document.isEmpty,

View File

@ -1,3 +1,4 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/build_context_extension.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/text_robot.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
@ -7,6 +8,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/wid
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
@ -15,8 +17,6 @@ import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:provider/provider.dart';
class AutoCompletionBlockKeys {
@ -169,9 +169,12 @@ class _AutoCompletionBlockComponentState
return FlowyTextField(
hintText: LocaleKeys.document_plugins_autoGeneratorHintText.tr(),
controller: controller,
maxLines: 3,
maxLines: 5,
focusNode: textFieldFocusNode,
autoFocus: false,
hintTextConstraints: const BoxConstraints(
maxHeight: double.infinity,
),
);
}

View File

@ -70,9 +70,12 @@ CharacterShortcutEvent insertChildNodeInsideToggleList = CharacterShortcutEvent(
// insert a toggle list block below the current toggle list block
transaction
..deleteText(node, selection.startIndex, slicedDelta.length)
..insertNode(
..insertNodes(
selection.start.path.next,
toggleListBlockNode(collapsed: true, delta: slicedDelta),
[
toggleListBlockNode(collapsed: true, delta: slicedDelta),
paragraphNode(),
],
)
..afterSelection = Selection.collapsed(
Position(path: selection.start.path.next, offset: 0),

View File

@ -106,16 +106,20 @@ extension ViewExtension on ViewPB {
FlowySvgData get iconData => layout.icon;
Future<List<ViewPB>> getAncestors({bool includeSelf = false}) async {
Future<List<ViewPB>> getAncestors({
bool includeSelf = false,
bool includeRoot = false,
}) async {
final ancestors = <ViewPB>[];
if (includeSelf) {
ancestors.add(this);
final self = await ViewBackendService.getView(id);
ancestors.add(self.getLeftOrNull<ViewPB>() ?? this);
}
var parent = await ViewBackendService.getView(parentViewId);
while (parent.isLeft()) {
// parent is not null
final view = parent.getLeftOrNull<ViewPB>();
if (view == null) {
if (view == null || (!includeRoot && view.parentViewId.isEmpty)) {
break;
}
ancestors.add(view);

View File

@ -18,6 +18,12 @@ class SidebarFolder extends StatelessWidget {
@override
Widget build(BuildContext context) {
// check if there is any duplicate views
final views = this.views.toSet().toList();
final favoriteViews = this.favoriteViews.toSet().toList();
assert(views.length == this.views.length);
assert(favoriteViews.length == favoriteViews.length);
return ValueListenableBuilder(
valueListenable: getIt<MenuSharedState>().notifier,
builder: (context, value, child) {
@ -27,6 +33,7 @@ class SidebarFolder extends StatelessWidget {
// favorite
if (favoriteViews.isNotEmpty) ...[
FavoriteFolder(
// remove the duplicate views
views: favoriteViews,
),
const VSpace(10),

View File

@ -55,6 +55,7 @@ void showSnackBarMessage(
}) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Theme.of(context).colorScheme.onSecondary,
action: !showCancel
? null
: SnackBarAction(
@ -66,7 +67,6 @@ void showSnackBarMessage(
),
content: FlowyText(
message,
color: Theme.of(context).colorScheme.onSurface,
),
),
);

View File

@ -44,8 +44,8 @@ void showEmojiPickerMenu(
builder: (context) => Material(
type: MaterialType.transparency,
child: Container(
width: 300,
height: 250,
width: 360,
height: 380,
padding: const EdgeInsets.all(4.0),
decoration: FlowyDecoration.decoration(
Theme.of(context).cardColor,

View File

@ -1,4 +1,5 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';
import 'package:appflowy/startup/tasks/app_window_size_manager.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/application/view/view_listener.dart';
@ -29,9 +30,7 @@ class _ViewTitleBarState extends State<ViewTitleBar> {
void initState() {
super.initState();
ancestors = widget.view.getAncestors(
includeSelf: true,
);
_reloadAncestors();
}
@override
@ -39,9 +38,7 @@ class _ViewTitleBarState extends State<ViewTitleBar> {
super.didUpdateWidget(oldWidget);
if (oldWidget.view.id != widget.view.id) {
ancestors = widget.view.getAncestors(
includeSelf: true,
);
_reloadAncestors();
}
}
@ -51,27 +48,57 @@ class _ViewTitleBarState extends State<ViewTitleBar> {
future: ancestors,
builder: ((context, snapshot) {
final ancestors = snapshot.data;
if (ancestors == null ||
snapshot.connectionState != ConnectionState.done) {
if (ancestors == null) {
return const SizedBox.shrink();
}
return Row(
const maxWidth = WindowSizeManager.minWindowWidth - 100;
final replacement = Row(
// refresh the view title bar when the ancestors changed
key: ValueKey(ancestors.hashCode),
children: _buildViewTitles(ancestors),
);
return LayoutBuilder(
builder: (context, constraints) {
return Visibility(
visible: constraints.maxWidth < maxWidth,
replacement: replacement,
// if the width is too small, only show one view title bar without the ancestors
child: _ViewTitle(
view: ancestors.last,
behavior: _ViewTitleBehavior.editable,
maxTitleWidth: constraints.maxWidth - 50.0,
onUpdated: () => setState(() => _reloadAncestors()),
),
);
},
);
}),
);
}
List<Widget> _buildViewTitles(List<ViewPB> views) {
// if the level is too deep, only show the last two view, the first one view and the root view
bool hasAddedEllipsis = false;
final children = <Widget>[];
for (var i = 0; i < views.length; i++) {
final view = views[i];
if (i >= 1 && i < views.length - 2) {
if (!hasAddedEllipsis) {
hasAddedEllipsis = true;
children.add(
const FlowyText.regular(' ... /'),
);
}
continue;
}
children.add(
_ViewTitle(
view: view,
behavior: i == views.length - 1
? _ViewTitleBehavior.editable // only the last one is editable
: _ViewTitleBehavior.uneditable, // others are not editable
onUpdated: () => setState(() => _reloadAncestors()),
),
);
if (i != views.length - 1) {
@ -81,6 +108,12 @@ class _ViewTitleBarState extends State<ViewTitleBar> {
}
return children;
}
void _reloadAncestors() {
ancestors = widget.view.getAncestors(
includeSelf: true,
);
}
}
enum _ViewTitleBehavior {
@ -92,10 +125,14 @@ class _ViewTitle extends StatefulWidget {
const _ViewTitle({
required this.view,
this.behavior = _ViewTitleBehavior.editable,
this.maxTitleWidth = 180,
required this.onUpdated,
});
final ViewPB view;
final _ViewTitleBehavior behavior;
final double maxTitleWidth;
final VoidCallback onUpdated;
@override
State<_ViewTitle> createState() => _ViewTitleState();
@ -124,6 +161,7 @@ class _ViewTitleState extends State<_ViewTitle> {
icon = view.icon.value;
_resetTextEditingController();
});
widget.onUpdated();
},
);
}
@ -156,7 +194,15 @@ class _ViewTitleState extends State<_ViewTitle> {
fontSize: 18.0,
),
const HSpace(2.0),
FlowyText.regular(name),
ConstrainedBox(
constraints: BoxConstraints(
maxWidth: widget.maxTitleWidth,
),
child: FlowyText.regular(
name,
overflow: TextOverflow.ellipsis,
),
),
],
);
@ -188,8 +234,8 @@ class _ViewTitleState extends State<_ViewTitle> {
defaultIcon: widget.view.defaultIcon(),
direction: PopoverDirection.bottomWithCenterAligned,
offset: const Offset(0, 18),
onSubmitted: (emoji, _) {
ViewBackendService.updateViewIcon(
onSubmitted: (emoji, _) async {
await ViewBackendService.updateViewIcon(
viewId: widget.view.id,
viewIcon: emoji,
);
@ -203,9 +249,9 @@ class _ViewTitleState extends State<_ViewTitle> {
child: FlowyTextField(
autoFocus: true,
controller: textEditingController,
onSubmitted: (text) {
onSubmitted: (text) async {
if (text.isNotEmpty && text != name) {
ViewBackendService.updateView(
await ViewBackendService.updateView(
viewId: widget.view.id,
name: text,
);

View File

@ -26,6 +26,7 @@ class FlowyTextField extends StatefulWidget {
final Widget? suffixIcon;
final BoxConstraints? prefixIconConstraints;
final BoxConstraints? suffixIconConstraints;
final BoxConstraints? hintTextConstraints;
const FlowyTextField({
super.key,
@ -50,6 +51,7 @@ class FlowyTextField extends StatefulWidget {
this.suffixIcon,
this.prefixIconConstraints,
this.suffixIconConstraints,
this.hintTextConstraints,
});
@override
@ -121,15 +123,22 @@ class FlowyTextFieldState extends State<FlowyTextField> {
},
onSubmitted: (text) => _onSubmitted(text),
onEditingComplete: widget.onEditingComplete,
minLines: 1,
maxLines: widget.maxLines,
maxLength: widget.maxLength,
maxLengthEnforcement: MaxLengthEnforcement.truncateAfterCompositionEnds,
style: widget.textStyle ?? Theme.of(context).textTheme.bodySmall,
textAlignVertical: TextAlignVertical.center,
keyboardType: TextInputType.multiline,
decoration: InputDecoration(
constraints: BoxConstraints(
maxHeight: widget.errorText?.isEmpty ?? true ? 32 : 58),
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
constraints: widget.hintTextConstraints ??
BoxConstraints(
maxHeight: widget.errorText?.isEmpty ?? true ? 32 : 58,
),
contentPadding: EdgeInsets.symmetric(
horizontal: 12,
vertical: widget.maxLines > 1 ? 12 : 0,
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline,

View File

@ -101,11 +101,14 @@ impl FolderOperationHandler for DocumentFolderOperation {
FutureResult::new(async move {
let mut write_guard = workspace_view_builder.write().await;
// Create a view named "⭐️ Getting started" with built-in README data.
// Create a view named "Getting started" with an icon ⭐️ and the built-in README data.
// Don't modify this code unless you know what you are doing.
write_guard
.with_view_builder(|view_builder| async {
let view = view_builder.with_name("⭐️ Getting started").build();
let view = view_builder
.with_name("Getting started")
.with_icon("⭐️")
.build();
// create a empty document
let json_str = include_str!("../../assets/read_me.json");
let document_pb = JsonToDocumentParser::json_str_to_document(json_str).unwrap();

View File

@ -96,6 +96,14 @@ impl ViewBuilder {
self
}
pub fn with_icon(mut self, icon: &str) -> Self {
self.icon = Some(ViewIcon {
ty: collab_folder::IconType::Emoji,
value: icon.to_string(),
});
self
}
/// Create a child view for the current view.
/// The view created by this builder will be the next level view of the current view.
pub async fn with_child_view_builder<F, O>(mut self, child_view_builder: F) -> Self