Merge branch 'main' into workspace-rename-icon

This commit is contained in:
Zack Fu Zi Xiang 2024-02-29 10:06:55 +08:00
commit 891543653d
No known key found for this signature in database
89 changed files with 1231 additions and 567 deletions

View File

@ -1,4 +1,3 @@
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart'; import 'package:integration_test/integration_test.dart';
@ -10,7 +9,7 @@ void main() {
group('text direction', () { group('text direction', () {
testWidgets( testWidgets(
'''no text direction items will be displayed in the default/LTR mode,and three text direction items will be displayed in the RTL mode.''', '''no text direction items will be displayed in the default/LTR mode, and three text direction items will be displayed when toggle is enabled.''',
(tester) async { (tester) async {
// combine the two tests into one to avoid the time-consuming process of initializing the app // combine the two tests into one to avoid the time-consuming process of initializing the app
await tester.initializeAppFlowy(); await tester.initializeAppFlowy();
@ -32,7 +31,7 @@ void main() {
'toolbar/text_direction_ltr', 'toolbar/text_direction_ltr',
'toolbar/text_direction_rtl', 'toolbar/text_direction_rtl',
]; ];
// no text direction items in default/LTR mode // no text direction items by default
var button = find.byWidgetPredicate( var button = find.byWidgetPredicate(
(widget) => (widget) =>
widget is SVGIconItemWidget && widget is SVGIconItemWidget &&
@ -41,7 +40,7 @@ void main() {
expect(button, findsNothing); expect(button, findsNothing);
// switch to the RTL mode // switch to the RTL mode
await tester.switchLayoutDirectionMode(LayoutDirection.rtlLayout); await tester.toggleEnableRTLToolbarItems();
await tester.editor.tapLineOfEditorAt(0); await tester.editor.tapLineOfEditorAt(0);
await tester.editor.updateSelection(selection); await tester.editor.updateSelection(selection);

View File

@ -1,12 +1,11 @@
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart'; import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_user.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_user.dart';
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart'; import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/direction_setting.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'base.dart'; import 'base.dart';
@ -76,31 +75,15 @@ extension AppFlowySettings on WidgetTester {
await pumpAndSettle(); await pumpAndSettle();
} }
// go to settings page and switch the layout direction // go to settings page and toggle enable RTL toolbar items
Future<void> switchLayoutDirectionMode( Future<void> toggleEnableRTLToolbarItems() async {
LayoutDirection layoutDirection,
) async {
await openSettings(); await openSettings();
await openSettingsPage(SettingsPage.appearance); await openSettingsPage(SettingsPage.appearance);
final button = find.byKey(const ValueKey('layout_direction_option_button')); final switchButton =
expect(button, findsOneWidget); find.byKey(EnableRTLToolbarItemsSetting.enableRTLSwitchKey);
await tapButton(button); expect(switchButton, findsOneWidget);
await tapButton(switchButton);
switch (layoutDirection) {
case LayoutDirection.ltrLayout:
final ltrButton = find.text(
LocaleKeys.settings_appearance_layoutDirection_ltr.tr(),
);
await tapButton(ltrButton);
break;
case LayoutDirection.rtlLayout:
final rtlButton = find.text(
LocaleKeys.settings_appearance_layoutDirection_rtl.tr(),
);
await tapButton(rtlButton);
break;
}
// tap anywhere to close the settings page // tap anywhere to close the settings page
await tapAt(Offset.zero); await tapAt(Offset.zero);

View File

@ -127,11 +127,13 @@ Future<T?> showMobileBottomSheet<T>(
children: [ children: [
...children, ...children,
Expanded( Expanded(
child: Scrollbar(
child: SingleChildScrollView( child: SingleChildScrollView(
controller: scrollController, controller: scrollController,
child: child, child: child,
), ),
), ),
),
], ],
); );
}, },

View File

@ -1,6 +1,9 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart';
import 'package:appflowy/plugins/database/board/application/board_bloc.dart'; import 'package:appflowy/plugins/database/board/application/board_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_type_extension.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_type_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
@ -8,7 +11,6 @@ import 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart';
import 'package:appflowy_board/appflowy_board.dart'; import 'package:appflowy_board/appflowy_board.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
@ -107,16 +109,18 @@ class _GroupCardHeaderState extends State<GroupCardHeader> {
splashRadius: 5, splashRadius: 5,
onPressed: () => showMobileBottomSheet( onPressed: () => showMobileBottomSheet(
context, context,
title: LocaleKeys.board_column_groupActions.tr(), showDragHandle: true,
showHeader: true, backgroundColor: Theme.of(context).colorScheme.surface,
showCloseButton: true, builder: (_) => SeparatedColumn(
builder: (_) { crossAxisAlignment: CrossAxisAlignment.stretch,
return Row( separatorBuilder: () => const Divider(
height: 8.5,
thickness: 0.5,
),
children: [ children: [
Expanded( MobileQuickActionButton(
child: BottomSheetActionWidget(
svg: FlowySvgs.edit_s,
text: LocaleKeys.board_column_renameColumn.tr(), text: LocaleKeys.board_column_renameColumn.tr(),
icon: FlowySvgs.edit_s,
onTap: () { onTap: () {
context.read<BoardBloc>().add( context.read<BoardBloc>().add(
BoardEvent.startEditingHeader( BoardEvent.startEditingHeader(
@ -126,12 +130,9 @@ class _GroupCardHeaderState extends State<GroupCardHeader> {
context.pop(); context.pop();
}, },
), ),
), MobileQuickActionButton(
const HSpace(8),
Expanded(
child: BottomSheetActionWidget(
svg: FlowySvgs.hide_s,
text: LocaleKeys.board_column_hideColumn.tr(), text: LocaleKeys.board_column_hideColumn.tr(),
icon: FlowySvgs.hide_s,
onTap: () { onTap: () {
context.read<BoardBloc>().add( context.read<BoardBloc>().add(
BoardEvent.toggleGroupVisibility( BoardEvent.toggleGroupVisibility(
@ -143,10 +144,8 @@ class _GroupCardHeaderState extends State<GroupCardHeader> {
context.pop(); context.pop();
}, },
), ),
),
], ],
); ),
},
), ),
), ),
IconButton( IconButton(

View File

@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
@ -5,10 +7,10 @@ import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/user/prelude.dart'; import 'package:appflowy/workspace/application/user/prelude.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../widgets/widgets.dart'; import '../widgets/widgets.dart';
import 'personal_info.dart'; import 'personal_info.dart';
class PersonalInfoSettingGroup extends StatelessWidget { class PersonalInfoSettingGroup extends StatelessWidget {
@ -34,7 +36,7 @@ class PersonalInfoSettingGroup extends StatelessWidget {
settingItemList: [ settingItemList: [
MobileSettingItem( MobileSettingItem(
name: userName, name: userName,
subtitle: isAuthEnabled subtitle: isAuthEnabled && userProfile.email.isNotEmpty
? Text( ? Text(
userProfile.email, userProfile.email,
style: theme.textTheme.bodyMedium?.copyWith( style: theme.textTheme.bodyMedium?.copyWith(

View File

@ -7,16 +7,16 @@ class CalendarSize {
static double get headerContainerPadding => 12 * scale; static double get headerContainerPadding => 12 * scale;
static EdgeInsets get contentInsets => EdgeInsets.fromLTRB( static EdgeInsets get contentInsets => EdgeInsets.fromLTRB(
GridSize.leadingHeaderPadding, GridSize.horizontalHeaderPadding,
CalendarSize.headerContainerPadding, CalendarSize.headerContainerPadding,
GridSize.leadingHeaderPadding, GridSize.horizontalHeaderPadding,
CalendarSize.headerContainerPadding, CalendarSize.headerContainerPadding,
); );
static EdgeInsets get contentInsetsMobile => EdgeInsets.fromLTRB( static EdgeInsets get contentInsetsMobile => EdgeInsets.fromLTRB(
GridSize.leadingHeaderPadding / 2, GridSize.horizontalHeaderPadding / 2,
0, 0,
GridSize.leadingHeaderPadding / 2, GridSize.horizontalHeaderPadding / 2,
0, 0,
); );

View File

@ -16,7 +16,7 @@ class GridLayout {
.reduce((value, element) => value + element); .reduce((value, element) => value + element);
return fieldsWidth + return fieldsWidth +
GridSize.leadingHeaderPadding + GridSize.horizontalHeaderPadding +
GridSize.trailHeaderPadding; GridSize.trailHeaderPadding;
} }
} }

View File

@ -7,19 +7,14 @@ class GridSize {
static double get scrollBarSize => 8 * scale; static double get scrollBarSize => 8 * scale;
static double get headerHeight => 40 * scale; static double get headerHeight => 40 * scale;
static double get footerHeight => 40 * scale; static double get footerHeight => 40 * scale;
static double get leadingHeaderPadding => static double get horizontalHeaderPadding =>
PlatformExtension.isDesktop ? 40 * scale : 20 * scale; PlatformExtension.isDesktop ? 40 * scale : 16 * scale;
static double get trailHeaderPadding => 140 * scale; static double get trailHeaderPadding => 140 * scale;
static double get headerContainerPadding => 0 * scale;
static double get cellHPadding => 10 * scale; static double get cellHPadding => 10 * scale;
static double get cellVPadding => 10 * scale; static double get cellVPadding => 10 * scale;
static double get popoverItemHeight => 26 * scale; static double get popoverItemHeight => 26 * scale;
static double get typeOptionSeparatorHeight => 4 * scale; static double get typeOptionSeparatorHeight => 4 * scale;
static EdgeInsets get headerContentInsets => EdgeInsets.symmetric(
horizontal: GridSize.headerContainerPadding,
vertical: GridSize.headerContainerPadding,
);
static EdgeInsets get cellContentInsets => EdgeInsets.symmetric( static EdgeInsets get cellContentInsets => EdgeInsets.symmetric(
horizontal: GridSize.cellHPadding, horizontal: GridSize.cellHPadding,
vertical: GridSize.cellVPadding, vertical: GridSize.cellVPadding,
@ -36,18 +31,13 @@ class GridSize {
const EdgeInsets.symmetric(horizontal: 8, vertical: 2); const EdgeInsets.symmetric(horizontal: 8, vertical: 2);
static EdgeInsets get footerContentInsets => EdgeInsets.fromLTRB( static EdgeInsets get footerContentInsets => EdgeInsets.fromLTRB(
GridSize.leadingHeaderPadding, GridSize.horizontalHeaderPadding,
GridSize.headerContainerPadding, 0,
PlatformExtension.isMobile PlatformExtension.isMobile ? GridSize.horizontalHeaderPadding : 0,
? GridSize.leadingHeaderPadding PlatformExtension.isMobile ? 100 : 0,
: GridSize.headerContainerPadding,
PlatformExtension.isMobile ? 100 : GridSize.headerContainerPadding,
); );
static EdgeInsets get contentInsets => EdgeInsets.fromLTRB( static EdgeInsets get contentInsets => EdgeInsets.symmetric(
GridSize.leadingHeaderPadding, horizontal: GridSize.horizontalHeaderPadding,
GridSize.headerContainerPadding,
GridSize.leadingHeaderPadding,
GridSize.headerContainerPadding,
); );
} }

View File

@ -157,12 +157,14 @@ class _GridPageContentState extends State<GridPageContent> {
final _scrollController = GridScrollController( final _scrollController = GridScrollController(
scrollGroupController: LinkedScrollControllerGroup(), scrollGroupController: LinkedScrollControllerGroup(),
); );
late final ScrollController headerScrollController; late final ScrollController contentScrollController;
late final ScrollController reorderableController;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
headerScrollController = _scrollController.linkHorizontalController(); contentScrollController = _scrollController.linkHorizontalController();
reorderableController = _scrollController.linkHorizontalController();
} }
@override @override
@ -196,7 +198,8 @@ class _GridPageContentState extends State<GridPageContent> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_GridHeader( _GridHeader(
headerScrollController: headerScrollController, contentScrollController: contentScrollController,
reorderableController: reorderableController,
), ),
_GridRows( _GridRows(
viewId: widget.view.id, viewId: widget.view.id,
@ -205,8 +208,8 @@ class _GridPageContentState extends State<GridPageContent> {
], ],
), ),
Positioned( Positioned(
bottom: 20, bottom: 16,
right: 20, right: 16,
child: getGridFabs(context), child: getGridFabs(context),
), ),
], ],
@ -216,9 +219,13 @@ class _GridPageContentState extends State<GridPageContent> {
} }
class _GridHeader extends StatelessWidget { class _GridHeader extends StatelessWidget {
const _GridHeader({required this.headerScrollController}); const _GridHeader({
required this.contentScrollController,
required this.reorderableController,
});
final ScrollController headerScrollController; final ScrollController contentScrollController;
final ScrollController reorderableController;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -226,7 +233,8 @@ class _GridHeader extends StatelessWidget {
builder: (context, state) { builder: (context, state) {
return MobileGridHeader( return MobileGridHeader(
viewId: state.viewId, viewId: state.viewId,
anchorScrollController: headerScrollController, contentScrollController: contentScrollController,
reorderableController: reorderableController,
); );
}, },
); );
@ -247,7 +255,7 @@ class _GridRows extends StatelessWidget {
return BlocBuilder<GridBloc, GridState>( return BlocBuilder<GridBloc, GridState>(
buildWhen: (previous, current) => previous.fields != current.fields, buildWhen: (previous, current) => previous.fields != current.fields,
builder: (context, state) { builder: (context, state) {
final double contentWidth = _getContentWidth(state.fields); final double contentWidth = getMobileGridContentWidth(state.fields);
return Expanded( return Expanded(
child: _WrapScrollView( child: _WrapScrollView(
scrollController: scrollController, scrollController: scrollController,
@ -277,14 +285,6 @@ class _GridRows extends StatelessWidget {
); );
} }
double _getContentWidth(List<FieldInfo> fields) {
final visibleFields = fields.where(
(field) =>
field.fieldSettings?.visibility != FieldVisibility.AlwaysHidden,
);
return (visibleFields.length + 1) * 200 + GridSize.leadingHeaderPadding * 2;
}
Widget _renderList( Widget _renderList(
BuildContext context, BuildContext context,
GridState state, GridState state,
@ -438,3 +438,11 @@ class _AddRowButton extends StatelessWidget {
); );
} }
} }
double getMobileGridContentWidth(List<FieldInfo> fields) {
final visibleFields = fields.where(
(field) => field.fieldSettings?.visibility != FieldVisibility.AlwaysHidden,
);
return (visibleFields.length + 1) * 200 +
GridSize.horizontalHeaderPadding * 2;
}

View File

@ -139,7 +139,7 @@ class _GridHeaderState extends State<_GridHeader> {
} }
Widget _cellLeading() { Widget _cellLeading() {
return SizedBox(width: GridSize.leadingHeaderPadding); return SizedBox(width: GridSize.horizontalHeaderPadding);
} }
} }
@ -158,7 +158,6 @@ class _CellTrailing extends StatelessWidget {
bottom: BorderSide(color: Theme.of(context).dividerColor), bottom: BorderSide(color: Theme.of(context).dividerColor),
), ),
), ),
padding: GridSize.headerContentInsets,
child: CreateFieldButton( child: CreateFieldButton(
viewId: viewId, viewId: viewId,
onFieldCreated: (fieldId) => context onFieldCreated: (fieldId) => context

View File

@ -13,17 +13,22 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../../layout/sizes.dart'; import '../../layout/sizes.dart';
import '../../mobile_grid_page.dart';
import 'mobile_field_button.dart'; import 'mobile_field_button.dart';
const double _kGridHeaderHeight = 50.0;
class MobileGridHeader extends StatefulWidget { class MobileGridHeader extends StatefulWidget {
const MobileGridHeader({ const MobileGridHeader({
super.key, super.key,
required this.viewId, required this.viewId,
required this.anchorScrollController, required this.contentScrollController,
required this.reorderableController,
}); });
final String viewId; final String viewId;
final ScrollController anchorScrollController; final ScrollController contentScrollController;
final ScrollController reorderableController;
@override @override
State<MobileGridHeader> createState() => _MobileGridHeaderState(); State<MobileGridHeader> createState() => _MobileGridHeaderState();
@ -41,30 +46,46 @@ class _MobileGridHeaderState extends State<MobileGridHeader> {
fieldController: fieldController, fieldController: fieldController,
)..add(const GridHeaderEvent.initial()); )..add(const GridHeaderEvent.initial());
}, },
child: SingleChildScrollView( child: Stack(
children: [
BlocBuilder<GridHeaderBloc, GridHeaderState>(
builder: (context, state) {
return SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
controller: widget.anchorScrollController, controller: widget.contentScrollController,
child: Row( child: Stack(
mainAxisSize: MainAxisSize.min,
children: [ children: [
HSpace(GridSize.leadingHeaderPadding), Positioned(
Stack( top: 0,
children: [ left: GridSize.horizontalHeaderPadding + 24,
Positioned(top: 0, left: 24, right: 24, child: _divider()), right: GridSize.horizontalHeaderPadding + 24,
Positioned(bottom: 0, left: 0, right: 0, child: _divider()), child: _divider(),
),
Positioned(
bottom: 0,
left: GridSize.horizontalHeaderPadding,
right: GridSize.horizontalHeaderPadding,
child: _divider(),
),
SizedBox( SizedBox(
height: 50, height: _kGridHeaderHeight,
width: getMobileGridContentWidth(state.fields),
),
],
),
);
},
),
SizedBox(
height: _kGridHeaderHeight,
child: _GridHeader( child: _GridHeader(
viewId: widget.viewId, viewId: widget.viewId,
fieldController: fieldController, fieldController: fieldController,
scrollController: widget.reorderableController,
), ),
), ),
], ],
), ),
const HSpace(20),
],
),
),
); );
} }
@ -81,10 +102,12 @@ class _GridHeader extends StatefulWidget {
const _GridHeader({ const _GridHeader({
required this.viewId, required this.viewId,
required this.fieldController, required this.fieldController,
required this.scrollController,
}); });
final String viewId; final String viewId;
final FieldController fieldController; final FieldController fieldController;
final ScrollController scrollController;
@override @override
State<_GridHeader> createState() => _GridHeaderState(); State<_GridHeader> createState() => _GridHeaderState();
@ -114,13 +137,16 @@ class _GridHeaderState extends State<_GridHeader> {
.toList(); .toList();
return ReorderableListView.builder( return ReorderableListView.builder(
scrollController: ScrollController(), scrollController: widget.scrollController,
shrinkWrap: true, shrinkWrap: true,
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
proxyDecorator: (child, index, anim) => Material( proxyDecorator: (child, index, anim) => Material(
color: Colors.transparent, color: Colors.transparent,
child: child, child: child,
), ),
padding: EdgeInsets.symmetric(
horizontal: GridSize.horizontalHeaderPadding,
),
header: firstField != null header: firstField != null
? MobileFieldButton.first( ? MobileFieldButton.first(
viewId: widget.viewId, viewId: widget.viewId,

View File

@ -68,7 +68,7 @@ class _MobileGridRowState extends State<MobileGridRow> {
builder: (context, state) { builder: (context, state) {
return Row( return Row(
children: [ children: [
SizedBox(width: GridSize.leadingHeaderPadding), SizedBox(width: GridSize.horizontalHeaderPadding),
Expanded( Expanded(
child: RowContent( child: RowContent(
fieldController: widget.databaseController.fieldController, fieldController: widget.databaseController.fieldController,
@ -163,7 +163,6 @@ class RowContent extends StatelessWidget {
Widget _finalCellDecoration(BuildContext context) { Widget _finalCellDecoration(BuildContext context) {
return Container( return Container(
width: 200, width: 200,
padding: GridSize.headerContentInsets,
constraints: const BoxConstraints(minHeight: 46), constraints: const BoxConstraints(minHeight: 46),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border( border: Border(

View File

@ -115,7 +115,7 @@ class _RowLeadingState extends State<_RowLeading> {
child: Consumer<RegionStateNotifier>( child: Consumer<RegionStateNotifier>(
builder: (context, state, _) { builder: (context, state, _) {
return SizedBox( return SizedBox(
width: GridSize.leadingHeaderPadding, width: GridSize.horizontalHeaderPadding,
child: state.onEnter ? _activeWidget() : null, child: state.onEnter ? _activeWidget() : null,
); );
}, },
@ -283,7 +283,6 @@ class RowContent extends StatelessWidget {
cursor: SystemMouseCursors.basic, cursor: SystemMouseCursors.basic,
child: Container( child: Container(
width: GridSize.trailHeaderPadding, width: GridSize.trailHeaderPadding,
padding: GridSize.headerContentInsets,
constraints: const BoxConstraints(minHeight: 46), constraints: const BoxConstraints(minHeight: 46),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border( border: Border(

View File

@ -57,7 +57,7 @@ class _DatabaseViewSettingContent extends StatelessWidget {
builder: (context, state) { builder: (context, state) {
return Padding( return Padding(
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
horizontal: GridSize.leadingHeaderPadding, horizontal: GridSize.horizontalHeaderPadding,
), ),
child: DecoratedBox( child: DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(

View File

@ -1,3 +1,4 @@
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
@ -21,8 +22,11 @@ class TabBarHeader extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( return Container(
height: 30, height: 30,
padding: EdgeInsets.symmetric(
horizontal: GridSize.horizontalHeaderPadding,
),
child: Stack( child: Stack(
children: [ children: [
Positioned( Positioned(

View File

@ -1,16 +1,17 @@
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/show_transition_bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/show_transition_bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/database/view/database_view_list.dart'; import 'package:appflowy/mobile/presentation/database/view/database_view_list.dart';
import 'package:appflowy/plugins/base/emoji/emoji_text.dart'; import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
import 'package:appflowy/plugins/database/application/tab_bar_bloc.dart'; import 'package:appflowy/plugins/database/application/tab_bar_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/widgets/setting/mobile_database_controls.dart'; import 'package:appflowy/plugins/database/widgets/setting/mobile_database_controls.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../../grid/presentation/grid_page.dart'; import '../../grid/presentation/grid_page.dart';
@ -26,11 +27,14 @@ class _MobileTabBarHeaderState extends State<MobileTabBarHeader> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Padding(
padding: const EdgeInsets.only(top: 14.0), padding: EdgeInsets.only(
left: GridSize.horizontalHeaderPadding,
top: 14.0,
right: GridSize.horizontalHeaderPadding - 5.0,
),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
HSpace(GridSize.leadingHeaderPadding),
const _DatabaseViewSelectorButton(), const _DatabaseViewSelectorButton(),
const Spacer(), const Spacer(),
BlocBuilder<DatabaseTabBarBloc, DatabaseTabBarState>( BlocBuilder<DatabaseTabBarBloc, DatabaseTabBarState>(

View File

@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/application/tab_bar_bloc.dart'; import 'package:appflowy/plugins/database/application/tab_bar_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/widgets/share_button.dart'; import 'package:appflowy/plugins/database/widgets/share_button.dart';
import 'package:appflowy/plugins/util.dart'; import 'package:appflowy/plugins/util.dart';
import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/startup/plugin/plugin.dart';
@ -112,19 +111,9 @@ class _DatabaseTabBarViewState extends State<DatabaseTabBarView> {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
if (PlatformExtension.isDesktop) { return PlatformExtension.isDesktop
return Padding( ? const TabBarHeader()
padding: EdgeInsets.symmetric( : const MobileTabBarHeader();
horizontal: GridSize.leadingHeaderPadding,
),
child: const TabBarHeader(),
);
} else {
return const Padding(
padding: EdgeInsets.only(right: 8),
child: MobileTabBarHeader(),
);
}
}, },
); );
}, },

View File

@ -259,7 +259,9 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
LayoutDirection.rtlLayout; LayoutDirection.rtlLayout;
final textDirection = isRTL ? TextDirection.rtl : TextDirection.ltr; final textDirection = isRTL ? TextDirection.rtl : TextDirection.ltr;
_setRTLToolbarItems(isRTL); _setRTLToolbarItems(
context.read<AppearanceSettingsCubit>().state.enableRtlToolbarItems,
);
final editor = Directionality( final editor = Directionality(
textDirection: textDirection, textDirection: textDirection,
@ -412,12 +414,12 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
); );
} }
void _setRTLToolbarItems(bool isRTL) { void _setRTLToolbarItems(bool enableRtlToolbarItems) {
final textDirectionItemIds = textDirectionItems.map((e) => e.id); final textDirectionItemIds = textDirectionItems.map((e) => e.id);
// clear all the text direction items // clear all the text direction items
toolbarItems.removeWhere((item) => textDirectionItemIds.contains(item.id)); toolbarItems.removeWhere((item) => textDirectionItemIds.contains(item.id));
// only show the rtl item when the layout direction is ltr. // only show the rtl item when the layout direction is ltr.
if (isRTL) { if (enableRtlToolbarItems) {
toolbarItems.addAll(textDirectionItems); toolbarItems.addAll(textDirectionItems);
} }
} }

View File

@ -1,10 +1,9 @@
import 'package:flutter/material.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
enum MentionType { enum MentionType {

View File

@ -141,7 +141,7 @@ class EditorMigration {
} }
const backgroundColor = 'backgroundColor'; const backgroundColor = 'backgroundColor';
if (attributes.containsKey(backgroundColor)) { if (attributes.containsKey(backgroundColor)) {
attributes[AppFlowyRichTextKeys.highlightColor] = attributes[AppFlowyRichTextKeys.backgroundColor] =
attributes[backgroundColor]; attributes[backgroundColor];
attributes.remove(backgroundColor); attributes.remove(backgroundColor);
} }

View File

@ -11,6 +11,9 @@ Future<T?> showEditLinkBottomSheet<T>(
return showMobileBottomSheet( return showMobileBottomSheet(
context, context,
showHeader: false, showHeader: false,
showCloseButton: false,
showDragHandle: true,
padding: const EdgeInsets.symmetric(horizontal: 16),
builder: (context) { builder: (context) {
return MobileBottomSheetEditLinkWidget( return MobileBottomSheetEditLinkWidget(
text: text, text: text,

View File

@ -71,7 +71,8 @@ class BIUSItems extends StatelessWidget {
setState(() {}); setState(() {});
}, },
icon: icon, icon: icon,
isSelected: editorState.isTextDecorationSelected(richTextKey), isSelected: editorState.isTextDecorationSelected(richTextKey) &&
editorState.toggledStyle[richTextKey] != false,
iconPadding: const EdgeInsets.symmetric( iconPadding: const EdgeInsets.symmetric(
vertical: 14.0, vertical: 14.0,
), ),

View File

@ -68,7 +68,7 @@ class BlockItems extends StatelessWidget {
enableTopRightRadius: false, enableTopRightRadius: false,
enableBottomRightRadius: false, enableBottomRightRadius: false,
onTap: () async { onTap: () async {
await editorState.convertBlockType(blockType); await _convert(blockType);
}, },
backgroundColor: theme.toolbarMenuItemBackgroundColor, backgroundColor: theme.toolbarMenuItemBackgroundColor,
icon: icon, icon: icon,
@ -196,4 +196,23 @@ class BlockItems extends StatelessWidget {
); );
editorState.service.keyboardService?.closeKeyboard(); editorState.service.keyboardService?.closeKeyboard();
} }
Future<void> _convert(String blockType) async {
await editorState.convertBlockType(
blockType,
selectionExtraInfo: {
selectionExtraInfoDoNotAttachTextService: true,
selectionExtraInfoDisableFloatingToolbar: true,
},
);
unawaited(
editorState.updateSelectionWithReason(
editorState.selection,
extraInfo: {
selectionExtraInfoDisableFloatingToolbar: true,
selectionExtraInfoDoNotAttachTextService: true,
},
),
);
}
} }

View File

@ -20,6 +20,8 @@ class ColorItem extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = ToolbarColorExtension.of(context); final theme = ToolbarColorExtension.of(context);
final selectedBackgroundColor = _getBackgroundColor(context);
return MobileToolbarMenuItemWrapper( return MobileToolbarMenuItemWrapper(
size: const Size(82, 52), size: const Size(82, 52),
onTap: () async { onTap: () async {
@ -42,10 +44,11 @@ class ColorItem extends StatelessWidget {
); );
}, },
icon: FlowySvgs.m_aa_color_s, icon: FlowySvgs.m_aa_color_s,
backgroundColor: theme.toolbarMenuItemBackgroundColor, backgroundColor:
isSelected: false, selectedBackgroundColor ?? theme.toolbarMenuItemBackgroundColor,
selectedBackgroundColor: selectedBackgroundColor,
isSelected: selectedBackgroundColor != null,
showRightArrow: true, showRightArrow: true,
enable: editorState.selection?.isCollapsed == false,
iconPadding: const EdgeInsets.only( iconPadding: const EdgeInsets.only(
top: 14.0, top: 14.0,
bottom: 14.0, bottom: 14.0,
@ -53,4 +56,33 @@ class ColorItem extends StatelessWidget {
), ),
); );
} }
Color? _getBackgroundColor(BuildContext context) {
final selection = editorState.selection;
if (selection == null) {
return null;
}
String? backgroundColor =
editorState.toggledStyle[AppFlowyRichTextKeys.backgroundColor];
if (backgroundColor == null) {
if (selection.isCollapsed && selection.startIndex != 0) {
backgroundColor = editorState.getDeltaAttributeValueInSelection<String>(
AppFlowyRichTextKeys.backgroundColor,
selection.copyWith(
start: selection.start.copyWith(
offset: selection.startIndex - 1,
),
),
);
} else {
backgroundColor = editorState.getDeltaAttributeValueInSelection<String>(
AppFlowyRichTextKeys.backgroundColor,
);
}
}
if (backgroundColor != null && int.tryParse(backgroundColor) != null) {
return Color(int.parse(backgroundColor));
}
return null;
}
} }

View File

@ -63,16 +63,9 @@ class _TextColorAndBackgroundColorState
extends State<_TextColorAndBackgroundColor> { extends State<_TextColorAndBackgroundColor> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final String? selectedTextColor = final String? selectedTextColor = _getColor(AppFlowyRichTextKeys.textColor);
widget.editorState.getDeltaAttributeValueInSelection(
AppFlowyRichTextKeys.textColor,
widget.selection,
);
final String? selectedBackgroundColor = final String? selectedBackgroundColor =
widget.editorState.getDeltaAttributeValueInSelection( _getColor(AppFlowyRichTextKeys.backgroundColor);
AppFlowyRichTextKeys.highlightColor,
widget.selection,
);
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -90,6 +83,13 @@ class _TextColorAndBackgroundColorState
selectedColor: selectedTextColor?.tryToColor(), selectedColor: selectedTextColor?.tryToColor(),
onSelectedColor: (textColor) async { onSelectedColor: (textColor) async {
final hex = textColor.alpha == 0 ? null : textColor.toHex(); final hex = textColor.alpha == 0 ? null : textColor.toHex();
final selection = widget.selection;
if (selection.isCollapsed) {
widget.editorState.updateToggledStyle(
AppFlowyRichTextKeys.textColor,
hex ?? '',
);
} else {
await widget.editorState.formatDelta( await widget.editorState.formatDelta(
widget.selection, widget.selection,
{ {
@ -101,6 +101,7 @@ class _TextColorAndBackgroundColorState
selectionExtraInfoDoNotAttachTextService: true, selectionExtraInfoDoNotAttachTextService: true,
}, },
); );
}
setState(() {}); setState(() {});
}, },
), ),
@ -119,10 +120,17 @@ class _TextColorAndBackgroundColorState
onSelectedColor: (backgroundColor) async { onSelectedColor: (backgroundColor) async {
final hex = final hex =
backgroundColor.alpha == 0 ? null : backgroundColor.toHex(); backgroundColor.alpha == 0 ? null : backgroundColor.toHex();
final selection = widget.selection;
if (selection.isCollapsed) {
widget.editorState.updateToggledStyle(
AppFlowyRichTextKeys.backgroundColor,
hex ?? '',
);
} else {
await widget.editorState.formatDelta( await widget.editorState.formatDelta(
widget.selection, widget.selection,
{ {
AppFlowyRichTextKeys.highlightColor: hex, AppFlowyRichTextKeys.backgroundColor: hex,
}, },
selectionExtraInfo: { selectionExtraInfo: {
selectionExtraInfoDisableFloatingToolbar: true, selectionExtraInfoDisableFloatingToolbar: true,
@ -130,12 +138,35 @@ class _TextColorAndBackgroundColorState
selectionExtraInfoDoNotAttachTextService: true, selectionExtraInfoDoNotAttachTextService: true,
}, },
); );
}
setState(() {}); setState(() {});
}, },
), ),
], ],
); );
} }
String? _getColor(String key) {
final selection = widget.selection;
String? color = widget.editorState.toggledStyle[key];
if (color == null) {
if (selection.isCollapsed && selection.startIndex != 0) {
color = widget.editorState.getDeltaAttributeValueInSelection<String>(
key,
selection.copyWith(
start: selection.start.copyWith(
offset: selection.startIndex - 1,
),
),
);
} else {
color = widget.editorState.getDeltaAttributeValueInSelection<String>(
key,
);
}
}
return color;
}
} }
class _BackgroundColors extends StatelessWidget { class _BackgroundColors extends StatelessWidget {

View File

@ -1,13 +1,12 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart';
import 'package:appflowy/mobile/presentation/setting/font/font_picker_screen.dart'; import 'package:appflowy/mobile/presentation/setting/font/font_picker_screen.dart';
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_toolbar_theme.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_toolbar_theme.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/util/google_font_family_extension.dart'; import 'package:appflowy/util/google_font_family_extension.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -23,15 +22,16 @@ class FontFamilyItem extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = ToolbarColorExtension.of(context); final theme = ToolbarColorExtension.of(context);
final fontFamily = editorState.getDeltaAttributeValueInSelection<String>( final fontFamily = _getCurrentSelectedFontFamilyName();
AppFlowyRichTextKeys.fontFamily,
);
final systemFonFamily = final systemFonFamily =
context.read<DocumentAppearanceCubit>().state.fontFamily; context.read<DocumentAppearanceCubit>().state.fontFamily;
return MobileToolbarMenuItemWrapper( return MobileToolbarMenuItemWrapper(
size: const Size(144, 52), size: const Size(144, 52),
onTap: () async { onTap: () async {
final selection = editorState.selection; final selection = editorState.selection;
if (selection == null) {
return;
}
// disable the floating toolbar // disable the floating toolbar
unawaited( unawaited(
editorState.updateSelectionWithReason( editorState.updateSelectionWithReason(
@ -46,12 +46,17 @@ class FontFamilyItem extends StatelessWidget {
final newFont = await context final newFont = await context
.read<GoRouter>() .read<GoRouter>()
.push<String>(FontPickerScreen.routeName); .push<String>(FontPickerScreen.routeName);
if (newFont != null && newFont != fontFamily) {
// if the selection is not collapsed, apply the font to the selection.
if (newFont != null && !selection.isCollapsed) {
if (newFont != fontFamily) {
await editorState.formatDelta(selection, { await editorState.formatDelta(selection, {
AppFlowyRichTextKeys.fontFamily: AppFlowyRichTextKeys.fontFamily:
GoogleFonts.getFont(newFont).fontFamily, GoogleFonts.getFont(newFont).fontFamily,
}); });
} }
}
// wait for the font picker screen to be dismissed. // wait for the font picker screen to be dismissed.
Future.delayed(const Duration(milliseconds: 250), () { Future.delayed(const Duration(milliseconds: 250), () {
// highlight the selected text again. // highlight the selected text again.
@ -62,13 +67,20 @@ class FontFamilyItem extends StatelessWidget {
selectionExtraInfoDisableMobileToolbarKey: false, selectionExtraInfoDisableMobileToolbarKey: false,
}, },
); );
// if the selection is collapsed, save the font for the next typing.
if (newFont != null && selection.isCollapsed) {
editorState.updateToggledStyle(
AppFlowyRichTextKeys.fontFamily,
GoogleFonts.getFont(newFont).fontFamily,
);
}
}); });
}, },
text: (fontFamily ?? systemFonFamily).parseFontFamilyName(), text: (fontFamily ?? systemFonFamily).parseFontFamilyName(),
fontFamily: fontFamily ?? systemFonFamily, fontFamily: fontFamily ?? systemFonFamily,
backgroundColor: theme.toolbarMenuItemBackgroundColor, backgroundColor: theme.toolbarMenuItemBackgroundColor,
isSelected: false, isSelected: false,
enable: editorState.selection?.isCollapsed == false, enable: true,
showRightArrow: true, showRightArrow: true,
iconPadding: const EdgeInsets.only( iconPadding: const EdgeInsets.only(
top: 14.0, top: 14.0,
@ -81,4 +93,28 @@ class FontFamilyItem extends StatelessWidget {
), ),
); );
} }
String? _getCurrentSelectedFontFamilyName() {
final toggleFontFamily =
editorState.toggledStyle[AppFlowyRichTextKeys.fontFamily];
if (toggleFontFamily is String && toggleFontFamily.isNotEmpty) {
return toggleFontFamily;
}
final selection = editorState.selection;
if (selection != null &&
selection.isCollapsed &&
selection.startIndex != 0) {
return editorState.getDeltaAttributeValueInSelection<String>(
AppFlowyRichTextKeys.fontFamily,
selection.copyWith(
start: selection.start.copyWith(
offset: selection.startIndex - 1,
),
),
);
}
return editorState.getDeltaAttributeValueInSelection<String>(
AppFlowyRichTextKeys.fontFamily,
);
}
} }

View File

@ -15,6 +15,7 @@ final aaToolbarItem = AppFlowyMobileToolbarItem(
pilotAtExpandedSelection: true, pilotAtExpandedSelection: true,
itemBuilder: (context, editorState, service, onMenu, _) { itemBuilder: (context, editorState, service, onMenu, _) {
return AppFlowyMobileToolbarIconItem( return AppFlowyMobileToolbarIconItem(
editorState: editorState,
isSelected: () => service.showMenuNotifier.value, isSelected: () => service.showMenuNotifier.value,
keepSelectedStatus: true, keepSelectedStatus: true,
icon: FlowySvgs.m_toolbar_aa_s, icon: FlowySvgs.m_toolbar_aa_s,

View File

@ -18,6 +18,7 @@ import 'package:go_router/go_router.dart';
final addBlockToolbarItem = AppFlowyMobileToolbarItem( final addBlockToolbarItem = AppFlowyMobileToolbarItem(
itemBuilder: (context, editorState, service, __, onAction) { itemBuilder: (context, editorState, service, __, onAction) {
return AppFlowyMobileToolbarIconItem( return AppFlowyMobileToolbarIconItem(
editorState: editorState,
icon: FlowySvgs.m_toolbar_add_s, icon: FlowySvgs.m_toolbar_add_s,
onTap: () { onTap: () {
final selection = editorState.selection; final selection = editorState.selection;
@ -83,7 +84,7 @@ Future<bool?> showAddBlockMenu(
} }
class _AddBlockMenu extends StatelessWidget { class _AddBlockMenu extends StatelessWidget {
_AddBlockMenu({ const _AddBlockMenu({
required this.selection, required this.selection,
required this.editorState, required this.editorState,
}); });
@ -91,25 +92,47 @@ class _AddBlockMenu extends StatelessWidget {
final Selection selection; final Selection selection;
final EditorState editorState; final EditorState editorState;
late final List<TypeOptionMenuItemValue<String>> typeOptionMenuItemValue = [ @override
Widget build(BuildContext context) {
return TypeOptionMenu<String>(
values: buildTypeOptionMenuItemValues(context),
scaleFactor: context.scale,
);
}
Future<void> _insertBlock(Node node) async {
AppGlobals.rootNavKey.currentContext?.pop(true);
Future.delayed(const Duration(milliseconds: 100), () {
editorState.insertBlockAfterCurrentSelection(
selection,
node,
);
});
}
List<TypeOptionMenuItemValue<String>> buildTypeOptionMenuItemValues(
BuildContext context,
) {
final colorMap = _colorMap(context);
return [
// heading 1 - 3 // heading 1 - 3
TypeOptionMenuItemValue( TypeOptionMenuItemValue(
value: HeadingBlockKeys.type, value: HeadingBlockKeys.type,
backgroundColor: const Color(0xFFBAC9FF), backgroundColor: colorMap[HeadingBlockKeys.type]!,
text: LocaleKeys.editor_heading1.tr(), text: LocaleKeys.editor_heading1.tr(),
icon: FlowySvgs.m_add_block_h1_s, icon: FlowySvgs.m_add_block_h1_s,
onTap: (_, __) => _insertBlock(headingNode(level: 1)), onTap: (_, __) => _insertBlock(headingNode(level: 1)),
), ),
TypeOptionMenuItemValue( TypeOptionMenuItemValue(
value: HeadingBlockKeys.type, value: HeadingBlockKeys.type,
backgroundColor: const Color(0xFFBAC9FF), backgroundColor: colorMap[HeadingBlockKeys.type]!,
text: LocaleKeys.editor_heading2.tr(), text: LocaleKeys.editor_heading2.tr(),
icon: FlowySvgs.m_add_block_h2_s, icon: FlowySvgs.m_add_block_h2_s,
onTap: (_, __) => _insertBlock(headingNode(level: 2)), onTap: (_, __) => _insertBlock(headingNode(level: 2)),
), ),
TypeOptionMenuItemValue( TypeOptionMenuItemValue(
value: HeadingBlockKeys.type, value: HeadingBlockKeys.type,
backgroundColor: const Color(0xFFBAC9FF), backgroundColor: colorMap[HeadingBlockKeys.type]!,
text: LocaleKeys.editor_heading3.tr(), text: LocaleKeys.editor_heading3.tr(),
icon: FlowySvgs.m_add_block_h3_s, icon: FlowySvgs.m_add_block_h3_s,
onTap: (_, __) => _insertBlock(headingNode(level: 3)), onTap: (_, __) => _insertBlock(headingNode(level: 3)),
@ -118,7 +141,7 @@ class _AddBlockMenu extends StatelessWidget {
// paragraph // paragraph
TypeOptionMenuItemValue( TypeOptionMenuItemValue(
value: ParagraphBlockKeys.type, value: ParagraphBlockKeys.type,
backgroundColor: const Color(0xFFBAC9FF), backgroundColor: colorMap[ParagraphBlockKeys.type]!,
text: LocaleKeys.editor_text.tr(), text: LocaleKeys.editor_text.tr(),
icon: FlowySvgs.m_add_block_paragraph_s, icon: FlowySvgs.m_add_block_paragraph_s,
onTap: (_, __) => _insertBlock(paragraphNode()), onTap: (_, __) => _insertBlock(paragraphNode()),
@ -127,7 +150,7 @@ class _AddBlockMenu extends StatelessWidget {
// checkbox // checkbox
TypeOptionMenuItemValue( TypeOptionMenuItemValue(
value: TodoListBlockKeys.type, value: TodoListBlockKeys.type,
backgroundColor: const Color(0xFF98F4CD), backgroundColor: colorMap[TodoListBlockKeys.type]!,
text: LocaleKeys.editor_checkbox.tr(), text: LocaleKeys.editor_checkbox.tr(),
icon: FlowySvgs.m_add_block_checkbox_s, icon: FlowySvgs.m_add_block_checkbox_s,
onTap: (_, __) => _insertBlock(todoListNode(checked: false)), onTap: (_, __) => _insertBlock(todoListNode(checked: false)),
@ -136,7 +159,7 @@ class _AddBlockMenu extends StatelessWidget {
// quote // quote
TypeOptionMenuItemValue( TypeOptionMenuItemValue(
value: QuoteBlockKeys.type, value: QuoteBlockKeys.type,
backgroundColor: const Color(0xFFFDEDA7), backgroundColor: colorMap[QuoteBlockKeys.type]!,
text: LocaleKeys.editor_quote.tr(), text: LocaleKeys.editor_quote.tr(),
icon: FlowySvgs.m_add_block_quote_s, icon: FlowySvgs.m_add_block_quote_s,
onTap: (_, __) => _insertBlock(quoteNode()), onTap: (_, __) => _insertBlock(quoteNode()),
@ -145,21 +168,21 @@ class _AddBlockMenu extends StatelessWidget {
// bulleted list, numbered list, toggle list // bulleted list, numbered list, toggle list
TypeOptionMenuItemValue( TypeOptionMenuItemValue(
value: BulletedListBlockKeys.type, value: BulletedListBlockKeys.type,
backgroundColor: const Color(0xFFFFB9EF), backgroundColor: colorMap[BulletedListBlockKeys.type]!,
text: LocaleKeys.editor_bulletedListShortForm.tr(), text: LocaleKeys.editor_bulletedListShortForm.tr(),
icon: FlowySvgs.m_add_block_bulleted_list_s, icon: FlowySvgs.m_add_block_bulleted_list_s,
onTap: (_, __) => _insertBlock(bulletedListNode()), onTap: (_, __) => _insertBlock(bulletedListNode()),
), ),
TypeOptionMenuItemValue( TypeOptionMenuItemValue(
value: NumberedListBlockKeys.type, value: NumberedListBlockKeys.type,
backgroundColor: const Color(0xFFFFB9EF), backgroundColor: colorMap[NumberedListBlockKeys.type]!,
text: LocaleKeys.editor_numberedListShortForm.tr(), text: LocaleKeys.editor_numberedListShortForm.tr(),
icon: FlowySvgs.m_add_block_numbered_list_s, icon: FlowySvgs.m_add_block_numbered_list_s,
onTap: (_, __) => _insertBlock(numberedListNode()), onTap: (_, __) => _insertBlock(numberedListNode()),
), ),
TypeOptionMenuItemValue( TypeOptionMenuItemValue(
value: ToggleListBlockKeys.type, value: ToggleListBlockKeys.type,
backgroundColor: const Color(0xFFFFB9EF), backgroundColor: colorMap[ToggleListBlockKeys.type]!,
text: LocaleKeys.editor_toggleListShortForm.tr(), text: LocaleKeys.editor_toggleListShortForm.tr(),
icon: FlowySvgs.m_add_block_toggle_s, icon: FlowySvgs.m_add_block_toggle_s,
onTap: (_, __) => _insertBlock(toggleListBlockNode()), onTap: (_, __) => _insertBlock(toggleListBlockNode()),
@ -167,8 +190,8 @@ class _AddBlockMenu extends StatelessWidget {
// image // image
TypeOptionMenuItemValue( TypeOptionMenuItemValue(
value: DividerBlockKeys.type, value: ImageBlockKeys.type,
backgroundColor: const Color(0xFF98F4CD), backgroundColor: colorMap[ImageBlockKeys.type]!,
text: LocaleKeys.editor_image.tr(), text: LocaleKeys.editor_image.tr(),
icon: FlowySvgs.m_add_block_image_s, icon: FlowySvgs.m_add_block_image_s,
onTap: (_, __) async { onTap: (_, __) async {
@ -183,7 +206,7 @@ class _AddBlockMenu extends StatelessWidget {
// date // date
TypeOptionMenuItemValue( TypeOptionMenuItemValue(
value: ParagraphBlockKeys.type, value: ParagraphBlockKeys.type,
backgroundColor: const Color(0xFF91EAF5), backgroundColor: colorMap['date']!,
text: LocaleKeys.editor_date.tr(), text: LocaleKeys.editor_date.tr(),
icon: FlowySvgs.m_add_block_date_s, icon: FlowySvgs.m_add_block_date_s,
onTap: (_, __) => _insertBlock(dateMentionNode()), onTap: (_, __) => _insertBlock(dateMentionNode()),
@ -192,7 +215,7 @@ class _AddBlockMenu extends StatelessWidget {
// divider // divider
TypeOptionMenuItemValue( TypeOptionMenuItemValue(
value: DividerBlockKeys.type, value: DividerBlockKeys.type,
backgroundColor: const Color(0xFF98F4CD), backgroundColor: colorMap[DividerBlockKeys.type]!,
text: LocaleKeys.editor_divider.tr(), text: LocaleKeys.editor_divider.tr(),
icon: FlowySvgs.m_add_block_divider_s, icon: FlowySvgs.m_add_block_divider_s,
onTap: (_, __) { onTap: (_, __) {
@ -206,21 +229,21 @@ class _AddBlockMenu extends StatelessWidget {
// callout, code, math equation // callout, code, math equation
TypeOptionMenuItemValue( TypeOptionMenuItemValue(
value: CalloutBlockKeys.type, value: CalloutBlockKeys.type,
backgroundColor: const Color(0xFFCABDFF), backgroundColor: colorMap[CalloutBlockKeys.type]!,
text: LocaleKeys.document_plugins_callout.tr(), text: LocaleKeys.document_plugins_callout.tr(),
icon: FlowySvgs.m_add_block_callout_s, icon: FlowySvgs.m_add_block_callout_s,
onTap: (_, __) => _insertBlock(calloutNode()), onTap: (_, __) => _insertBlock(calloutNode()),
), ),
TypeOptionMenuItemValue( TypeOptionMenuItemValue(
value: CodeBlockKeys.type, value: CodeBlockKeys.type,
backgroundColor: const Color(0xFFCABDFF), backgroundColor: colorMap[CodeBlockKeys.type]!,
text: LocaleKeys.editor_codeBlockShortForm.tr(), text: LocaleKeys.editor_codeBlockShortForm.tr(),
icon: FlowySvgs.m_add_block_code_s, icon: FlowySvgs.m_add_block_code_s,
onTap: (_, __) => _insertBlock(codeBlockNode()), onTap: (_, __) => _insertBlock(codeBlockNode()),
), ),
TypeOptionMenuItemValue( TypeOptionMenuItemValue(
value: MathEquationBlockKeys.type, value: MathEquationBlockKeys.type,
backgroundColor: const Color(0xFFCABDFF), backgroundColor: colorMap[MathEquationBlockKeys.type]!,
text: LocaleKeys.editor_mathEquationShortForm.tr(), text: LocaleKeys.editor_mathEquationShortForm.tr(),
icon: FlowySvgs.m_add_block_formula_s, icon: FlowySvgs.m_add_block_formula_s,
onTap: (_, __) { onTap: (_, __) {
@ -231,23 +254,42 @@ class _AddBlockMenu extends StatelessWidget {
}, },
), ),
]; ];
@override
Widget build(BuildContext context) {
return TypeOptionMenu<String>(
values: typeOptionMenuItemValue,
scaleFactor: context.scale,
);
} }
Future<void> _insertBlock(Node node) async { Map<String, Color> _colorMap(BuildContext context) {
AppGlobals.rootNavKey.currentContext?.pop(true); final isDarkMode = Theme.of(context).brightness == Brightness.dark;
Future.delayed(const Duration(milliseconds: 100), () { if (isDarkMode) {
editorState.insertBlockAfterCurrentSelection( return {
selection, HeadingBlockKeys.type: const Color(0xFF5465A1),
node, ParagraphBlockKeys.type: const Color(0xFF5465A1),
); TodoListBlockKeys.type: const Color(0xFF4BB299),
}); QuoteBlockKeys.type: const Color(0xFFBAAC74),
BulletedListBlockKeys.type: const Color(0xFFA35F94),
NumberedListBlockKeys.type: const Color(0xFFA35F94),
ToggleListBlockKeys.type: const Color(0xFFA35F94),
ImageBlockKeys.type: const Color(0xFFBAAC74),
'date': const Color(0xFF40AAB8),
DividerBlockKeys.type: const Color(0xFF4BB299),
CalloutBlockKeys.type: const Color(0xFF66599B),
CodeBlockKeys.type: const Color(0xFF66599B),
MathEquationBlockKeys.type: const Color(0xFF66599B),
};
}
return {
HeadingBlockKeys.type: const Color(0xFFBECCFF),
ParagraphBlockKeys.type: const Color(0xFFBECCFF),
TodoListBlockKeys.type: const Color(0xFF98F4CD),
QuoteBlockKeys.type: const Color(0xFFFDEDA7),
BulletedListBlockKeys.type: const Color(0xFFFFB9EF),
NumberedListBlockKeys.type: const Color(0xFFFFB9EF),
ToggleListBlockKeys.type: const Color(0xFFFFB9EF),
ImageBlockKeys.type: const Color(0xFFFDEDA7),
'date': const Color(0xFF91EAF5),
DividerBlockKeys.type: const Color(0xFF98F4CD),
CalloutBlockKeys.type: const Color(0xFFCABDFF),
CodeBlockKeys.type: const Color(0xFFCABDFF),
MathEquationBlockKeys.type: const Color(0xFFCABDFF),
};
} }
} }

View File

@ -270,6 +270,11 @@ class _MobileToolbarState extends State<_MobileToolbar>
widget.editorState.selection = null; widget.editorState.selection = null;
} }
// if the menu is shown and the height is not 0, we need to close the menu
if (showMenuNotifier.value && height != 0) {
closeItemMenu();
}
if (canUpdateCachedKeyboardHeight) { if (canUpdateCachedKeyboardHeight) {
cachedKeyboardHeight.value = height; cachedKeyboardHeight.value = height;
} }

View File

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_toolbar_theme.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_toolbar_theme.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar.dart';
@ -42,7 +44,9 @@ class AppFlowyMobileToolbarIconItem extends StatefulWidget {
this.keepSelectedStatus = false, this.keepSelectedStatus = false,
this.iconBuilder, this.iconBuilder,
this.isSelected, this.isSelected,
this.shouldListenToToggledStyle = false,
required this.onTap, required this.onTap,
required this.editorState,
}); });
final FlowySvgData? icon; final FlowySvgData? icon;
@ -50,6 +54,8 @@ class AppFlowyMobileToolbarIconItem extends StatefulWidget {
final VoidCallback onTap; final VoidCallback onTap;
final WidgetBuilder? iconBuilder; final WidgetBuilder? iconBuilder;
final bool Function()? isSelected; final bool Function()? isSelected;
final bool shouldListenToToggledStyle;
final EditorState editorState;
@override @override
State<AppFlowyMobileToolbarIconItem> createState() => State<AppFlowyMobileToolbarIconItem> createState() =>
@ -59,12 +65,28 @@ class AppFlowyMobileToolbarIconItem extends StatefulWidget {
class _AppFlowyMobileToolbarIconItemState class _AppFlowyMobileToolbarIconItemState
extends State<AppFlowyMobileToolbarIconItem> { extends State<AppFlowyMobileToolbarIconItem> {
bool isSelected = false; bool isSelected = false;
StreamSubscription? _subscription;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
isSelected = widget.isSelected?.call() ?? false; isSelected = widget.isSelected?.call() ?? false;
if (widget.shouldListenToToggledStyle) {
widget.editorState.toggledStyleNotifier.addListener(_rebuild);
_subscription = widget.editorState.transactionStream.listen((_) {
_rebuild();
});
}
}
@override
void dispose() {
if (widget.shouldListenToToggledStyle) {
widget.editorState.toggledStyleNotifier.removeListener(_rebuild);
_subscription?.cancel();
}
super.dispose();
} }
@override @override
@ -85,15 +107,7 @@ class _AppFlowyMobileToolbarIconItemState
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTap: () { onTap: () {
widget.onTap(); widget.onTap();
if (widget.keepSelectedStatus && widget.isSelected == null) { _rebuild();
setState(() {
isSelected = !isSelected;
});
} else {
setState(() {
isSelected = widget.isSelected?.call() ?? false;
});
}
}, },
child: Container( child: Container(
width: 48, width: 48,
@ -111,4 +125,12 @@ class _AppFlowyMobileToolbarIconItemState
), ),
); );
} }
void _rebuild() {
setState(() {
isSelected = (widget.keepSelectedStatus && widget.isSelected == null)
? !isSelected
: widget.isSelected?.call() ?? false;
});
}
} }

View File

@ -6,9 +6,13 @@ import 'package:appflowy_editor/appflowy_editor.dart';
final boldToolbarItem = AppFlowyMobileToolbarItem( final boldToolbarItem = AppFlowyMobileToolbarItem(
itemBuilder: (context, editorState, _, __, onAction) { itemBuilder: (context, editorState, _, __, onAction) {
return AppFlowyMobileToolbarIconItem( return AppFlowyMobileToolbarIconItem(
isSelected: () => editorState.isTextDecorationSelected( editorState: editorState,
shouldListenToToggledStyle: true,
isSelected: () =>
editorState.isTextDecorationSelected(
AppFlowyRichTextKeys.bold, AppFlowyRichTextKeys.bold,
), ) &&
editorState.toggledStyle[AppFlowyRichTextKeys.bold] != false,
icon: FlowySvgs.m_toolbar_bold_s, icon: FlowySvgs.m_toolbar_bold_s,
onTap: () async => editorState.toggleAttribute( onTap: () async => editorState.toggleAttribute(
AppFlowyRichTextKeys.bold, AppFlowyRichTextKeys.bold,
@ -23,7 +27,8 @@ final boldToolbarItem = AppFlowyMobileToolbarItem(
final italicToolbarItem = AppFlowyMobileToolbarItem( final italicToolbarItem = AppFlowyMobileToolbarItem(
itemBuilder: (context, editorState, _, __, onAction) { itemBuilder: (context, editorState, _, __, onAction) {
return AppFlowyMobileToolbarIconItem( return AppFlowyMobileToolbarIconItem(
// keepSelectedStatus: true, editorState: editorState,
shouldListenToToggledStyle: true,
isSelected: () => editorState.isTextDecorationSelected( isSelected: () => editorState.isTextDecorationSelected(
AppFlowyRichTextKeys.italic, AppFlowyRichTextKeys.italic,
), ),
@ -41,6 +46,8 @@ final italicToolbarItem = AppFlowyMobileToolbarItem(
final underlineToolbarItem = AppFlowyMobileToolbarItem( final underlineToolbarItem = AppFlowyMobileToolbarItem(
itemBuilder: (context, editorState, _, __, onAction) { itemBuilder: (context, editorState, _, __, onAction) {
return AppFlowyMobileToolbarIconItem( return AppFlowyMobileToolbarIconItem(
editorState: editorState,
shouldListenToToggledStyle: true,
isSelected: () => editorState.isTextDecorationSelected( isSelected: () => editorState.isTextDecorationSelected(
AppFlowyRichTextKeys.underline, AppFlowyRichTextKeys.underline,
), ),
@ -58,6 +65,8 @@ final underlineToolbarItem = AppFlowyMobileToolbarItem(
final colorToolbarItem = AppFlowyMobileToolbarItem( final colorToolbarItem = AppFlowyMobileToolbarItem(
itemBuilder: (context, editorState, service, __, onAction) { itemBuilder: (context, editorState, service, __, onAction) {
return AppFlowyMobileToolbarIconItem( return AppFlowyMobileToolbarIconItem(
editorState: editorState,
shouldListenToToggledStyle: true,
icon: FlowySvgs.m_toolbar_color_s, icon: FlowySvgs.m_toolbar_color_s,
onTap: () { onTap: () {
service.closeKeyboard(); service.closeKeyboard();

View File

@ -6,6 +6,8 @@ final todoListToolbarItem = AppFlowyMobileToolbarItem(
itemBuilder: (context, editorState, _, __, onAction) { itemBuilder: (context, editorState, _, __, onAction) {
final isSelected = editorState.isBlockTypeSelected(TodoListBlockKeys.type); final isSelected = editorState.isBlockTypeSelected(TodoListBlockKeys.type);
return AppFlowyMobileToolbarIconItem( return AppFlowyMobileToolbarIconItem(
editorState: editorState,
shouldListenToToggledStyle: true,
keepSelectedStatus: true, keepSelectedStatus: true,
isSelected: () => isSelected, isSelected: () => isSelected,
icon: FlowySvgs.m_toolbar_checkbox_s, icon: FlowySvgs.m_toolbar_checkbox_s,

View File

@ -4,6 +4,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.da
final moreToolbarItem = AppFlowyMobileToolbarItem( final moreToolbarItem = AppFlowyMobileToolbarItem(
itemBuilder: (context, editorState, _, __, onAction) { itemBuilder: (context, editorState, _, __, onAction) {
return AppFlowyMobileToolbarIconItem( return AppFlowyMobileToolbarIconItem(
editorState: editorState,
icon: FlowySvgs.m_toolbar_more_s, icon: FlowySvgs.m_toolbar_more_s,
onTap: () {}, onTap: () {},
); );

View File

@ -8,6 +8,7 @@ final undoToolbarItem = AppFlowyMobileToolbarItem(
itemBuilder: (context, editorState, _, __, onAction) { itemBuilder: (context, editorState, _, __, onAction) {
final theme = ToolbarColorExtension.of(context); final theme = ToolbarColorExtension.of(context);
return AppFlowyMobileToolbarIconItem( return AppFlowyMobileToolbarIconItem(
editorState: editorState,
iconBuilder: (context) { iconBuilder: (context) {
final canUndo = editorState.undoManager.undoStack.isNonEmpty; final canUndo = editorState.undoManager.undoStack.isNonEmpty;
return FlowySvg( return FlowySvg(
@ -26,6 +27,7 @@ final redoToolbarItem = AppFlowyMobileToolbarItem(
itemBuilder: (context, editorState, _, __, onAction) { itemBuilder: (context, editorState, _, __, onAction) {
final theme = ToolbarColorExtension.of(context); final theme = ToolbarColorExtension.of(context);
return AppFlowyMobileToolbarIconItem( return AppFlowyMobileToolbarIconItem(
editorState: editorState,
iconBuilder: (context) { iconBuilder: (context) {
final canRedo = editorState.undoManager.redoStack.isNonEmpty; final canRedo = editorState.undoManager.redoStack.isNonEmpty;
return FlowySvg( return FlowySvg(

View File

@ -11,6 +11,7 @@ class MobileToolbarMenuItemWrapper extends StatelessWidget {
this.icon, this.icon,
this.text, this.text,
this.backgroundColor, this.backgroundColor,
this.selectedBackgroundColor,
this.enable, this.enable,
this.fontFamily, this.fontFamily,
required this.isSelected, required this.isSelected,
@ -40,6 +41,7 @@ class MobileToolbarMenuItemWrapper extends StatelessWidget {
final bool showDownArrow; final bool showDownArrow;
final bool showRightArrow; final bool showRightArrow;
final Color? backgroundColor; final Color? backgroundColor;
final Color? selectedBackgroundColor;
final EdgeInsets textPadding; final EdgeInsets textPadding;
@override @override
@ -90,7 +92,8 @@ class MobileToolbarMenuItemWrapper extends StatelessWidget {
alignment: text != null ? Alignment.centerLeft : Alignment.center, alignment: text != null ? Alignment.centerLeft : Alignment.center,
decoration: BoxDecoration( decoration: BoxDecoration(
color: isSelected color: isSelected
? theme.toolbarMenuItemSelectedBackgroundColor ? (selectedBackgroundColor ??
theme.toolbarMenuItemSelectedBackgroundColor)
: backgroundColor, : backgroundColor,
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
topLeft: enableTopLeftRadius ? radius : Radius.zero, topLeft: enableTopLeftRadius ? radius : Radius.zero,

View File

@ -42,6 +42,7 @@ class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
appearanceSettings.monospaceFont, appearanceSettings.monospaceFont,
appearanceSettings.layoutDirection, appearanceSettings.layoutDirection,
appearanceSettings.textDirection, appearanceSettings.textDirection,
appearanceSettings.enableRtlToolbarItems,
appearanceSettings.locale, appearanceSettings.locale,
appearanceSettings.isMenuCollapsed, appearanceSettings.isMenuCollapsed,
appearanceSettings.menuOffset, appearanceSettings.menuOffset,
@ -134,6 +135,12 @@ class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
emit(state.copyWith(textDirection: textDirection)); emit(state.copyWith(textDirection: textDirection));
} }
void setEnableRTLToolbarItems(bool value) {
_appearanceSettings.enableRtlToolbarItems = value;
_saveAppearanceSettings();
emit(state.copyWith(enableRtlToolbarItems: value));
}
/// Update selected font in the user's settings and emit an updated state /// Update selected font in the user's settings and emit an updated state
/// with the font name. /// with the font name.
void setFontFamily(String fontFamilyName) { void setFontFamily(String fontFamilyName) {
@ -365,6 +372,7 @@ class AppearanceSettingsState with _$AppearanceSettingsState {
required String monospaceFont, required String monospaceFont,
required LayoutDirection layoutDirection, required LayoutDirection layoutDirection,
required AppFlowyTextDirection? textDirection, required AppFlowyTextDirection? textDirection,
required bool enableRtlToolbarItems,
required Locale locale, required Locale locale,
required bool isMenuCollapsed, required bool isMenuCollapsed,
required double menuOffset, required double menuOffset,
@ -383,6 +391,7 @@ class AppearanceSettingsState with _$AppearanceSettingsState {
String monospaceFont, String monospaceFont,
LayoutDirectionPB layoutDirectionPB, LayoutDirectionPB layoutDirectionPB,
TextDirectionPB? textDirectionPB, TextDirectionPB? textDirectionPB,
bool enableRtlToolbarItems,
LocaleSettingsPB localePB, LocaleSettingsPB localePB,
bool isMenuCollapsed, bool isMenuCollapsed,
double menuOffset, double menuOffset,
@ -399,6 +408,7 @@ class AppearanceSettingsState with _$AppearanceSettingsState {
monospaceFont: monospaceFont, monospaceFont: monospaceFont,
layoutDirection: LayoutDirection.fromLayoutDirectionPB(layoutDirectionPB), layoutDirection: LayoutDirection.fromLayoutDirectionPB(layoutDirectionPB),
textDirection: AppFlowyTextDirection.fromTextDirectionPB(textDirectionPB), textDirection: AppFlowyTextDirection.fromTextDirectionPB(textDirectionPB),
enableRtlToolbarItems: enableRtlToolbarItems,
themeMode: _themeModeFromPB(themeModePB), themeMode: _themeModeFromPB(themeModePB),
locale: Locale(localePB.languageCode, localePB.countryCode), locale: Locale(localePB.languageCode, localePB.countryCode),
isMenuCollapsed: isMenuCollapsed, isMenuCollapsed: isMenuCollapsed,

View File

@ -1,5 +1,3 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
@ -7,6 +5,7 @@ import 'package:appflowy/workspace/application/settings/appearance/appearance_cu
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'theme_setting_entry_template.dart'; import 'theme_setting_entry_template.dart';
@ -26,7 +25,6 @@ class LayoutDirectionSetting extends StatelessWidget {
hint: LocaleKeys.settings_appearance_layoutDirection_hint.tr(), hint: LocaleKeys.settings_appearance_layoutDirection_hint.tr(),
trailing: [ trailing: [
FlowySettingValueDropDown( FlowySettingValueDropDown(
key: const ValueKey('layout_direction_option_button'),
currentValue: _layoutDirectionLabelText(currentLayoutDirection), currentValue: _layoutDirectionLabelText(currentLayoutDirection),
popupBuilder: (context) => Column( popupBuilder: (context) => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -142,3 +140,34 @@ class TextDirectionSetting extends StatelessWidget {
} }
} }
} }
class EnableRTLToolbarItemsSetting extends StatelessWidget {
const EnableRTLToolbarItemsSetting({
super.key,
});
static const enableRTLSwitchKey = ValueKey('enable_rtl_toolbar_items_switch');
@override
Widget build(BuildContext context) {
return FlowySettingListTile(
label: LocaleKeys.settings_appearance_enableRTLToolbarItems.tr(),
trailing: [
Switch(
key: enableRTLSwitchKey,
value: context
.read<AppearanceSettingsCubit>()
.state
.enableRtlToolbarItems,
splashRadius: 0,
activeColor: Theme.of(context).colorScheme.primary,
onChanged: (value) {
context
.read<AppearanceSettingsCubit>()
.setEnableRTLToolbarItems(value);
},
),
],
);
}
}

View File

@ -53,6 +53,7 @@ class SettingsAppearanceView extends StatelessWidget {
TextDirectionSetting( TextDirectionSetting(
currentTextDirection: state.textDirection, currentTextDirection: state.textDirection,
), ),
const EnableRTLToolbarItemsSetting(),
const Divider(), const Divider(),
DateFormatSetting( DateFormatSetting(
currentFormat: state.dateFormat, currentFormat: state.dateFormat,

View File

@ -8,7 +8,6 @@ import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widget
import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/font_size_action.dart'; import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/font_size_action.dart';
import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/view_meta_info.dart'; import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/view_meta_info.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@ -35,13 +34,11 @@ class MoreViewActions extends StatefulWidget {
class _MoreViewActionsState extends State<MoreViewActions> { class _MoreViewActionsState extends State<MoreViewActions> {
late final List<Widget> viewActions; late final List<Widget> viewActions;
late final UserDateFormatPB dateFormat;
final popoverMutex = PopoverMutex(); final popoverMutex = PopoverMutex();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
dateFormat = context.read<AppearanceSettingsCubit>().state.dateFormat;
viewActions = ViewActionType.values viewActions = ViewActionType.values
.map( .map(
(type) => ViewAction( (type) => ViewAction(
@ -61,11 +58,15 @@ class _MoreViewActionsState extends State<MoreViewActions> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final appearanceSettings = context.watch<AppearanceSettingsCubit>().state;
final dateFormat = appearanceSettings.dateFormat;
final timeFormat = appearanceSettings.timeFormat;
return BlocBuilder<ViewInfoBloc, ViewInfoState>( return BlocBuilder<ViewInfoBloc, ViewInfoState>(
builder: (context, state) { builder: (context, state) {
return AppFlowyPopover( return AppFlowyPopover(
mutex: popoverMutex, mutex: popoverMutex,
constraints: BoxConstraints.loose(const Size(200, 400)), constraints: BoxConstraints.loose(const Size(210, 400)),
offset: const Offset(0, 30), offset: const Offset(0, 30),
popupBuilder: (_) { popupBuilder: (_) {
final actions = [ final actions = [
@ -79,6 +80,7 @@ class _MoreViewActionsState extends State<MoreViewActions> {
const Divider(height: 4), const Divider(height: 4),
ViewMetaInfo( ViewMetaInfo(
dateFormat: dateFormat, dateFormat: dateFormat,
timeFormat: timeFormat,
documentCounters: state.documentCounters, documentCounters: state.documentCounters,
createdAt: state.createdAt, createdAt: state.createdAt,
), ),

View File

@ -12,11 +12,13 @@ class ViewMetaInfo extends StatelessWidget {
const ViewMetaInfo({ const ViewMetaInfo({
super.key, super.key,
required this.dateFormat, required this.dateFormat,
required this.timeFormat,
this.documentCounters, this.documentCounters,
this.createdAt, this.createdAt,
}); });
final UserDateFormatPB dateFormat; final UserDateFormatPB dateFormat;
final UserTimeFormatPB timeFormat;
final Counters? documentCounters; final Counters? documentCounters;
final DateTime? createdAt; final DateTime? createdAt;
@ -47,8 +49,9 @@ class ViewMetaInfo extends StatelessWidget {
if (documentCounters != null) const VSpace(2), if (documentCounters != null) const VSpace(2),
FlowyText.regular( FlowyText.regular(
LocaleKeys.moreAction_createdAt.tr( LocaleKeys.moreAction_createdAt.tr(
args: [dateFormat.formatDate(createdAt!, false)], args: [dateFormat.formatDate(createdAt!, true, timeFormat)],
), ),
maxLines: 2,
color: Theme.of(context).hintColor, color: Theme.of(context).hintColor,
), ),
], ],

View File

@ -53,8 +53,8 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: "9d90053" ref: d4d35c0
resolved-ref: "9d9005366ba8383e93029c4f2f006f9ff8d1cb08" resolved-ref: d4d35c0d103a5d1bddf68181fcfaf9f75b0fccb5
url: "https://github.com/AppFlowy-IO/appflowy-editor.git" url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
source: git source: git
version: "2.3.2" version: "2.3.2"

View File

@ -167,7 +167,7 @@ dependency_overrides:
appflowy_editor: appflowy_editor:
git: git:
url: https://github.com/AppFlowy-IO/appflowy-editor.git url: https://github.com/AppFlowy-IO/appflowy-editor.git
ref: "9d90053" ref: "d4d35c0"
sheet: sheet:
git: git:

View File

@ -106,6 +106,13 @@ export const useFiltersCount = () => {
); );
}; };
export function useStaticTypeOption<T>(fieldId: string) {
const context = useContext(DatabaseContext);
const typeOptions = context.typeOptions;
return typeOptions[fieldId] as T;
}
export function useTypeOption<T>(fieldId: string) { export function useTypeOption<T>(fieldId: string) {
const context = useContext(DatabaseContext); const context = useContext(DatabaseContext);
const typeOptions = useSnapshot(context.typeOptions); const typeOptions = useSnapshot(context.typeOptions);

View File

@ -15,6 +15,7 @@ import ExpandRecordModal from '$app/components/database/components/edit_record/E
import { subscribeNotifications } from '$app/application/notification'; import { subscribeNotifications } from '$app/application/notification';
import { Page } from '$app_reducers/pages/slice'; import { Page } from '$app_reducers/pages/slice';
import { getPage } from '$app/application/folder/page.service'; import { getPage } from '$app/application/folder/page.service';
import './database.scss';
interface Props { interface Props {
selectedViewId?: string; selectedViewId?: string;

View File

@ -8,9 +8,11 @@ interface Props {
export const DatabaseCollection = ({ open }: Props) => { export const DatabaseCollection = ({ open }: Props) => {
return ( return (
<div className={`flex items-center gap-2 px-16 ${!open ? 'hidden' : 'py-3'}`}> <div className={`database-collection w-full px-[64px] ${!open ? 'hidden' : 'py-3'}`}>
<div className={'flex w-full items-center gap-2 overflow-x-auto overflow-y-hidden '}>
<Sorts /> <Sorts />
<Filters /> <Filters />
</div> </div>
</div>
); );
}; };

View File

@ -1,20 +1,20 @@
import React, { FC, useMemo, useState } from 'react'; import React, { FC, useMemo, useState } from 'react';
import { import {
Filter as FilterType,
Field as FieldData,
UndeterminedFilter,
TextFilterData,
SelectFilterData,
NumberFilterData,
CheckboxFilterData, CheckboxFilterData,
ChecklistFilterData, ChecklistFilterData,
DateFilterData, DateFilterData,
Field as FieldData,
Filter as FilterType,
NumberFilterData,
SelectFilterData,
TextFilterData,
UndeterminedFilter,
} from '$app/application/database'; } from '$app/application/database';
import { Chip, Popover } from '@mui/material'; import { Chip, Popover } from '@mui/material';
import { Property } from '$app/components/database/components/property'; import { Property } from '$app/components/database/components/property';
import { ReactComponent as DropDownSvg } from '$app/assets/dropdown.svg'; import { ReactComponent as DropDownSvg } from '$app/assets/dropdown.svg';
import TextFilter from './text_filter/TextFilter'; import TextFilter from './text_filter/TextFilter';
import { FieldType } from '@/services/backend'; import { CheckboxFilterConditionPB, ChecklistFilterConditionPB, FieldType } from '@/services/backend';
import FilterActions from '$app/components/database/components/filter/FilterActions'; import FilterActions from '$app/components/database/components/filter/FilterActions';
import { updateFilter } from '$app/application/database/filter/filter_service'; import { updateFilter } from '$app/application/database/filter/filter_service';
import { useViewId } from '$app/hooks'; import { useViewId } from '$app/hooks';
@ -22,6 +22,11 @@ import SelectFilter from './select_filter/SelectFilter';
import DateFilter from '$app/components/database/components/filter/date_filter/DateFilter'; import DateFilter from '$app/components/database/components/filter/date_filter/DateFilter';
import FilterConditionSelect from '$app/components/database/components/filter/FilterConditionSelect'; import FilterConditionSelect from '$app/components/database/components/filter/FilterConditionSelect';
import TextFilterValue from '$app/components/database/components/filter/text_filter/TextFilterValue';
import SelectFilterValue from '$app/components/database/components/filter/select_filter/SelectFilterValue';
import NumberFilterValue from '$app/components/database/components/filter/number_filter/NumberFilterValue';
import { useTranslation } from 'react-i18next';
import DateFilterValue from '$app/components/database/components/filter/date_filter/DateFilterValue';
interface Props { interface Props {
filter: FilterType; filter: FilterType;
@ -57,6 +62,7 @@ const getFilterComponent = (field: FieldData) => {
function Filter({ filter, field }: Props) { function Filter({ filter, field }: Props) {
const viewId = useViewId(); const viewId = useViewId();
const { t } = useTranslation();
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const open = Boolean(anchorEl); const open = Boolean(anchorEl);
const handleClick = (e: React.MouseEvent<HTMLElement>) => { const handleClick = (e: React.MouseEvent<HTMLElement>) => {
@ -70,7 +76,10 @@ function Filter({ filter, field }: Props) {
const onDataChange = async (data: UndeterminedFilter['data']) => { const onDataChange = async (data: UndeterminedFilter['data']) => {
const newFilter = { const newFilter = {
...filter, ...filter,
data, data: {
...(filter.data || {}),
...data,
},
} as UndeterminedFilter; } as UndeterminedFilter;
try { try {
@ -105,14 +114,42 @@ function Filter({ filter, field }: Props) {
} }
}, [field, filter]); }, [field, filter]);
const conditionValue = useMemo(() => {
switch (field.type) {
case FieldType.RichText:
case FieldType.URL:
return <TextFilterValue data={filter.data as TextFilterData} />;
case FieldType.SingleSelect:
case FieldType.MultiSelect:
return <SelectFilterValue data={filter.data as SelectFilterData} fieldId={field.id} />;
case FieldType.Number:
return <NumberFilterValue data={filter.data as NumberFilterData} />;
case FieldType.Checkbox:
return (filter.data as CheckboxFilterData).condition === CheckboxFilterConditionPB.IsChecked
? t('grid.checkboxFilter.isChecked')
: t('grid.checkboxFilter.isUnchecked');
case FieldType.Checklist:
return (filter.data as ChecklistFilterData).condition === ChecklistFilterConditionPB.IsComplete
? t('grid.checklistFilter.isComplete')
: t('grid.checklistFilter.isIncomplted');
case FieldType.DateTime:
case FieldType.LastEditedTime:
case FieldType.CreatedTime:
return <DateFilterValue data={filter.data as DateFilterData} />;
default:
return '';
}
}, [field.id, field.type, filter.data, t]);
return ( return (
<> <>
<Chip <Chip
clickable clickable
variant='outlined' variant='outlined'
label={ label={
<div className={'flex items-center justify-center gap-1'}> <div className={'flex items-center justify-between gap-1'}>
<Property field={field} /> <Property className={'flex flex-1 items-center'} field={field} />
<span className={'max-w-[120px] truncate'}>{conditionValue}</span>
<DropDownSvg className={'h-6 w-6'} /> <DropDownSvg className={'h-6 w-6'} />
</div> </div>
} }

View File

@ -29,9 +29,9 @@ function Filters() {
}; };
return ( return (
<div className={'flex items-center justify-center gap-2 text-text-title'}> <div className={'flex flex-1 items-center gap-2 text-text-title'}>
{options.map(({ filter, field }) => (field ? <Filter key={filter.id} filter={filter} field={field} /> : null))} {options.map(({ filter, field }) => (field ? <Filter key={filter.id} filter={filter} field={field} /> : null))}
<Button size={'small'} onClick={handleClick} color={'inherit'} startIcon={<AddSvg />}> <Button size={'small'} className={'min-w-[100px]'} onClick={handleClick} color={'inherit'} startIcon={<AddSvg />}>
{t('grid.settings.addFilter')} {t('grid.settings.addFilter')}
</Button> </Button>
<FilterFieldsMenu <FilterFieldsMenu

View File

@ -27,23 +27,19 @@ function DateFilter({ filter, field, onChange }: Props) {
const condition = filter.data.condition; const condition = filter.data.condition;
const isRange = condition === DateFilterConditionPB.DateWithIn; const isRange = condition === DateFilterConditionPB.DateWithIn;
const timestamp = useMemo(() => { const timestamp = useMemo(() => {
const now = Date.now() / 1000;
if (isRange) { if (isRange) {
return filter.data.start ? filter.data.start : now; return filter.data.start;
} }
return filter.data.timestamp ? filter.data.timestamp : now; return filter.data.timestamp;
}, [filter.data.start, filter.data.timestamp, isRange]); }, [filter.data.start, filter.data.timestamp, isRange]);
const endTimestamp = useMemo(() => { const endTimestamp = useMemo(() => {
const now = Date.now() / 1000;
if (isRange) { if (isRange) {
return filter.data.end ? filter.data.end : now; return filter.data.end;
} }
return now; return;
}, [filter.data.end, isRange]); }, [filter.data.end, isRange]);
const timeFormat = useMemo(() => { const timeFormat = useMemo(() => {
@ -64,7 +60,7 @@ function DateFilter({ filter, field, onChange }: Props) {
onChange({ onChange({
condition, condition,
timestamp: date, timestamp: date,
start: date, start: endDate ? date : undefined,
end: endDate, end: endDate,
}); });
}} }}
@ -81,7 +77,7 @@ function DateFilter({ filter, field, onChange }: Props) {
onChange({ onChange({
condition, condition,
timestamp: date, timestamp: date,
start: date, start: endDate ? date : undefined,
end: endDate, end: endDate,
}); });
}} }}

View File

@ -0,0 +1,52 @@
import React, { useMemo } from 'react';
import { DateFilterData } from '$app/application/database';
import { useTranslation } from 'react-i18next';
import dayjs from 'dayjs';
import { DateFilterConditionPB } from '@/services/backend';
function DateFilterValue({ data }: { data: DateFilterData }) {
const { t } = useTranslation();
const value = useMemo(() => {
if (!data.timestamp) return '';
let startStr = '';
let endStr = '';
if (data.start) {
const end = data.end ?? data.start;
const moreThanOneYear = dayjs.unix(end).diff(dayjs.unix(data.start), 'year') > 1;
const format = moreThanOneYear ? 'MMM D, YYYY' : 'MMM D';
startStr = dayjs.unix(data.start).format(format);
endStr = dayjs.unix(end).format(format);
}
const timestamp = dayjs.unix(data.timestamp).format('MMM D');
switch (data.condition) {
case DateFilterConditionPB.DateIs:
return `: ${timestamp}`;
case DateFilterConditionPB.DateBefore:
return `: ${t('grid.dateFilter.choicechipPrefix.before')} ${timestamp}`;
case DateFilterConditionPB.DateAfter:
return `: ${t('grid.dateFilter.choicechipPrefix.after')} ${timestamp}`;
case DateFilterConditionPB.DateOnOrBefore:
return `: ${t('grid.dateFilter.choicechipPrefix.onOrBefore')} ${timestamp}`;
case DateFilterConditionPB.DateOnOrAfter:
return `: ${t('grid.dateFilter.choicechipPrefix.onOrAfter')} ${timestamp}`;
case DateFilterConditionPB.DateWithIn:
return `: ${startStr} - ${endStr}`;
case DateFilterConditionPB.DateIsEmpty:
return `: ${t('grid.dateFilter.choicechipPrefix.isEmpty')}`;
case DateFilterConditionPB.DateIsNotEmpty:
return `: ${t('grid.dateFilter.choicechipPrefix.isNotEmpty')}`;
default:
return '';
}
}, [data, t]);
return <>{value}</>;
}
export default DateFilterValue;

View File

@ -0,0 +1,39 @@
import React, { useMemo } from 'react';
import { NumberFilterData } from '$app/application/database';
import { NumberFilterConditionPB } from '@/services/backend';
import { useTranslation } from 'react-i18next';
function NumberFilterValue({ data }: { data: NumberFilterData }) {
const { t } = useTranslation();
const value = useMemo(() => {
if (!data.content) {
return '';
}
const content = parseInt(data.content);
switch (data.condition) {
case NumberFilterConditionPB.Equal:
return `= ${content}`;
case NumberFilterConditionPB.NotEqual:
return `!= ${content}`;
case NumberFilterConditionPB.GreaterThan:
return `> ${content}`;
case NumberFilterConditionPB.GreaterThanOrEqualTo:
return `>= ${content}`;
case NumberFilterConditionPB.LessThan:
return `< ${content}`;
case NumberFilterConditionPB.LessThanOrEqualTo:
return `<= ${content}`;
case NumberFilterConditionPB.NumberIsEmpty:
return t('grid.textFilter.isEmpty');
case NumberFilterConditionPB.NumberIsNotEmpty:
return t('grid.textFilter.isNotEmpty');
}
}, [data.condition, data.content, t]);
return <>{value}</>;
}
export default NumberFilterValue;

View File

@ -0,0 +1,38 @@
import React, { useMemo } from 'react';
import { SelectFilterData, SelectTypeOption } from '$app/application/database';
import { useStaticTypeOption } from '$app/components/database';
import { useTranslation } from 'react-i18next';
import { SelectOptionConditionPB } from '@/services/backend';
function SelectFilterValue({ data, fieldId }: { data: SelectFilterData; fieldId: string }) {
const typeOption = useStaticTypeOption<SelectTypeOption>(fieldId);
const { t } = useTranslation();
const value = useMemo(() => {
if (!data.optionIds?.length) return '';
const options = data.optionIds
.map((optionId) => {
const option = typeOption?.options?.find((option) => option.id === optionId);
return option?.name;
})
.join(', ');
switch (data.condition) {
case SelectOptionConditionPB.OptionIs:
return `: ${options}`;
case SelectOptionConditionPB.OptionIsNot:
return `: ${t('grid.textFilter.choicechipPrefix.isNot')} ${options}`;
case SelectOptionConditionPB.OptionIsEmpty:
return `: ${t('grid.textFilter.choicechipPrefix.isEmpty')}`;
case SelectOptionConditionPB.OptionIsNotEmpty:
return `: ${t('grid.textFilter.choicechipPrefix.isNotEmpty')}`;
default:
return '';
}
}, [data.condition, data.optionIds, t, typeOption?.options]);
return <>{value}</>;
}
export default SelectFilterValue;

View File

@ -0,0 +1,34 @@
import { useMemo } from 'react';
import { TextFilterData } from '$app/application/database';
import { TextFilterConditionPB } from '@/services/backend';
import { useTranslation } from 'react-i18next';
function TextFilterValue({ data }: { data: TextFilterData }) {
const { t } = useTranslation();
const value = useMemo(() => {
if (!data.content) return '';
switch (data.condition) {
case TextFilterConditionPB.Contains:
case TextFilterConditionPB.Is:
return `: ${data.content}`;
case TextFilterConditionPB.DoesNotContain:
case TextFilterConditionPB.IsNot:
return `: ${t('grid.textFilter.choicechipPrefix.isNot')} ${data.content}`;
case TextFilterConditionPB.StartsWith:
return `: ${t('grid.textFilter.choicechipPrefix.startWith')} ${data.content}`;
case TextFilterConditionPB.EndsWith:
return `: ${t('grid.textFilter.choicechipPrefix.endWith')} ${data.content}`;
case TextFilterConditionPB.TextIsEmpty:
return `: ${t('grid.textFilter.choicechipPrefix.isEmpty')}`;
case TextFilterConditionPB.TextIsNotEmpty:
return `: ${t('grid.textFilter.choicechipPrefix.isNotEmpty')}`;
default:
return '';
}
}, [t, data]);
return <>{value}</>;
}
export default TextFilterValue;

View File

@ -10,6 +10,7 @@ export interface FieldProps {
menuOpened?: boolean; menuOpened?: boolean;
onOpenMenu?: (id: string) => void; onOpenMenu?: (id: string) => void;
onCloseMenu?: (id: string) => void; onCloseMenu?: (id: string) => void;
className?: string;
} }
const initialAnchorOrigin: PopoverOrigin = { const initialAnchorOrigin: PopoverOrigin = {
@ -22,7 +23,7 @@ const initialTransformOrigin: PopoverOrigin = {
horizontal: 'left', horizontal: 'left',
}; };
export const Property: FC<FieldProps> = ({ field, onCloseMenu, menuOpened }) => { export const Property: FC<FieldProps> = ({ field, onCloseMenu, className, menuOpened }) => {
const ref = useRef<HTMLDivElement | null>(null); const ref = useRef<HTMLDivElement | null>(null);
const [anchorPosition, setAnchorPosition] = useState< const [anchorPosition, setAnchorPosition] = useState<
| { | {
@ -63,7 +64,7 @@ export const Property: FC<FieldProps> = ({ field, onCloseMenu, menuOpened }) =>
return ( return (
<> <>
<div ref={ref} className='flex w-full items-center px-2'> <div ref={ref} className={className ? className : `flex w-full items-center px-2`}>
<ProppertyTypeSvg className='mr-1 text-base' type={field.type} /> <ProppertyTypeSvg className='mr-1 text-base' type={field.type} />
<span className='flex-1 truncate text-left text-xs'>{field.name}</span> <span className='flex-1 truncate text-left text-xs'>{field.name}</span>
</div> </div>

View File

@ -0,0 +1,6 @@
.database-collection {
::-webkit-scrollbar {
width: 0px;
height: 0px;
}
}

View File

@ -1,7 +1,6 @@
import { ReactEditor } from 'slate-react'; import { ReactEditor } from 'slate-react';
import { Editor, Element as SlateElement, NodeEntry, Range, Transforms } from 'slate'; import { Editor, Element, Element as SlateElement, NodeEntry, Range, Transforms } from 'slate';
import { EditorInlineNodeType, FormulaNode } from '$app/application/document/document.types'; import { EditorInlineNodeType, FormulaNode } from '$app/application/document/document.types';
import { isMarkActive } from '$app/components/editor/command/mark';
export function insertFormula(editor: ReactEditor, formula?: string) { export function insertFormula(editor: ReactEditor, formula?: string) {
if (editor.selection) { if (editor.selection) {
@ -80,5 +79,11 @@ export function unwrapFormula(editor: ReactEditor) {
} }
export function isFormulaActive(editor: ReactEditor) { export function isFormulaActive(editor: ReactEditor) {
return isMarkActive(editor, EditorInlineNodeType.Formula); const [match] = editor.nodes({
match: (n) => {
return !Editor.isEditor(n) && Element.isElement(n) && n.type === EditorInlineNodeType.Formula;
},
});
return Boolean(match);
} }

View File

@ -229,6 +229,16 @@ export const CustomEditor = {
return !!match; return !!match;
}, },
formulaActiveNode(editor: ReactEditor) {
const [match] = editor.nodes({
match: (n) => {
return !Editor.isEditor(n) && Element.isElement(n) && n.type === EditorInlineNodeType.Formula;
},
});
return match ? (match as NodeEntry<FormulaNode>) : undefined;
},
isMentionActive(editor: ReactEditor) { isMentionActive(editor: ReactEditor) {
const [match] = editor.nodes({ const [match] = editor.nodes({
match: (n) => { match: (n) => {
@ -519,6 +529,14 @@ export const CustomEditor = {
return editor.isEmpty(textNode); return editor.isEmpty(textNode);
}, },
includeInlineBlocks: (editor: ReactEditor) => {
const [match] = Editor.nodes(editor, {
match: (n) => Element.isElement(n) && editor.isInline(n),
});
return Boolean(match);
},
getNodeTextContent(node: Node): string { getNodeTextContent(node: Node): string {
if (Element.isElement(node) && node.type === EditorInlineNodeType.Formula) { if (Element.isElement(node) && node.type === EditorInlineNodeType.Formula) {
return (node as FormulaNode).data || ''; return (node as FormulaNode).data || '';

View File

@ -1,5 +1,5 @@
import { ReactEditor } from 'slate-react'; import { ReactEditor } from 'slate-react';
import { Editor, Text, Range } from 'slate'; import { Editor, Text, Range, Element } from 'slate';
import { EditorInlineNodeType, EditorMarkFormat } from '$app/application/document/document.types'; import { EditorInlineNodeType, EditorMarkFormat } from '$app/application/document/document.types';
export function toggleMark( export function toggleMark(
@ -33,18 +33,13 @@ export function isMarkActive(editor: ReactEditor, format: EditorMarkFormat | Edi
const isExpanded = Range.isExpanded(selection); const isExpanded = Range.isExpanded(selection);
if (isExpanded) { if (isExpanded) {
const matches = Array.from(getSelectionNodeEntry(editor) || []); const texts = getSelectionTexts(editor);
return matches.every((match) => {
const [node] = match;
return texts.every((node) => {
const { text, ...attributes } = node; const { text, ...attributes } = node;
if (!text) { if (!text) return true;
return true; return Boolean((attributes as Record<string, boolean | string>)[format]);
}
return !!(attributes as Record<string, boolean | string>)[format];
}); });
} }
@ -53,10 +48,12 @@ export function isMarkActive(editor: ReactEditor, format: EditorMarkFormat | Edi
return marks ? !!marks[format] : false; return marks ? !!marks[format] : false;
} }
function getSelectionNodeEntry(editor: ReactEditor) { function getSelectionTexts(editor: ReactEditor) {
const selection = editor.selection; const selection = editor.selection;
if (!selection) return null; if (!selection) return [];
const texts: Text[] = [];
const isExpanded = Range.isExpanded(selection); const isExpanded = Range.isExpanded(selection);
@ -73,16 +70,25 @@ function getSelectionNodeEntry(editor: ReactEditor) {
} }
} }
return Editor.nodes(editor, { Array.from(
match: Text.isText, Editor.nodes(editor, {
at: { at: {
anchor, anchor,
focus, focus,
}, },
})
).forEach((match) => {
const node = match[0] as Element;
if (Text.isText(node)) {
texts.push(node);
} else if (Editor.isInline(editor, node)) {
texts.push(...(node.children as Text[]));
}
}); });
} }
return null; return texts;
} }
/** /**
@ -97,13 +103,11 @@ export function getAllMarks(editor: ReactEditor) {
const isExpanded = Range.isExpanded(selection); const isExpanded = Range.isExpanded(selection);
if (isExpanded) { if (isExpanded) {
const matches = Array.from(getSelectionNodeEntry(editor) || []); const texts = getSelectionTexts(editor);
const marks: Record<string, string | boolean> = {}; const marks: Record<string, string | boolean> = {};
matches.forEach((match) => { texts.forEach((node) => {
const [node] = match;
Object.entries(node).forEach(([key, value]) => { Object.entries(node).forEach(([key, value]) => {
if (key !== 'text') { if (key !== 'text') {
marks[key] = value; marks[key] = value;

View File

@ -8,7 +8,7 @@ function BulletedListIcon({ block: _, className }: { block: BulletedListNode; cl
e.preventDefault(); e.preventDefault();
}} }}
contentEditable={false} contentEditable={false}
className={`${className} bulleted-icon flex w-[23px] justify-center pr-1 font-medium`} className={`${className} bulleted-icon flex min-w-[23px] justify-center pr-1 font-medium`}
/> />
); );
} }

View File

@ -36,7 +36,7 @@ function NumberListIcon({ block, className }: { block: NumberedListNode; classNa
}} }}
contentEditable={false} contentEditable={false}
data-number={index} data-number={index}
className={`${className} numbered-icon flex w-[23px] justify-center pr-1 font-medium`} className={`${className} numbered-icon flex w-[23px] min-w-[23px] justify-center pr-1 font-medium`}
/> />
); );
} }

View File

@ -16,7 +16,7 @@ export const InlineFormula = memo(
const { popoverOpen = false, setRange, openPopover, closePopover } = useEditorInlineBlockState('formula'); const { popoverOpen = false, setRange, openPopover, closePopover } = useEditorInlineBlockState('formula');
const anchor = useRef<HTMLSpanElement | null>(null); const anchor = useRef<HTMLSpanElement | null>(null);
const selected = useSelected(); const selected = useSelected();
const open = popoverOpen && selected; const open = Boolean(popoverOpen && selected);
const handleClick = useCallback( const handleClick = useCallback(
(e: MouseEvent<HTMLSpanElement>) => { (e: MouseEvent<HTMLSpanElement>) => {

View File

@ -3,7 +3,7 @@ import { MutableRefObject, useCallback, useEffect, useMemo, useRef, useState } f
import { getSelectionPosition } from '$app/components/editor/components/tools/selection_toolbar/utils'; import { getSelectionPosition } from '$app/components/editor/components/tools/selection_toolbar/utils';
import debounce from 'lodash-es/debounce'; import debounce from 'lodash-es/debounce';
import { CustomEditor } from '$app/components/editor/command'; import { CustomEditor } from '$app/components/editor/command';
import { BaseRange, Editor, Range as SlateRange } from 'slate'; import { BaseRange, Range as SlateRange } from 'slate';
import { useDecorateDispatch } from '$app/components/editor/stores/decorate'; import { useDecorateDispatch } from '$app/components/editor/stores/decorate';
const DELAY = 300; const DELAY = 300;
@ -118,9 +118,21 @@ export function useSelectionToolbar(ref: MutableRefObject<HTMLDivElement | null>
const { selection } = editor; const { selection } = editor;
if (!isFocusedEditor || !selection || SlateRange.isCollapsed(selection) || Editor.string(editor, selection) === '') { const close = () => {
debounceRecalculatePosition.cancel(); debounceRecalculatePosition.cancel();
closeToolbar(); closeToolbar();
};
if (!isFocusedEditor || !selection || SlateRange.isCollapsed(selection)) {
close();
return;
}
// There has a bug which the text of selection is empty when the selection include inline blocks
const isEmptyText = !CustomEditor.includeInlineBlocks(editor) && editor.string(selection) === '';
if (isEmptyText) {
close();
return; return;
} }

View File

@ -11,6 +11,7 @@ export function Bold() {
const { t } = useTranslation(); const { t } = useTranslation();
const editor = useSlateStatic(); const editor = useSlateStatic();
const isActivated = CustomEditor.isMarkActive(editor, EditorMarkFormat.Bold); const isActivated = CustomEditor.isMarkActive(editor, EditorMarkFormat.Bold);
const modifier = useMemo(() => getHotKey(EditorMarkFormat.Bold).modifier, []); const modifier = useMemo(() => getHotKey(EditorMarkFormat.Bold).modifier, []);
const onClick = useCallback(() => { const onClick = useCallback(() => {
CustomEditor.toggleMark(editor, { CustomEditor.toggleMark(editor, {

View File

@ -11,20 +11,28 @@ export function Formula() {
const editor = useSlateStatic(); const editor = useSlateStatic();
const isActivatedMention = CustomEditor.isMentionActive(editor); const isActivatedMention = CustomEditor.isMentionActive(editor);
const formulaMatch = CustomEditor.formulaActiveNode(editor);
const isActivated = !isActivatedMention && CustomEditor.isFormulaActive(editor); const isActivated = !isActivatedMention && CustomEditor.isFormulaActive(editor);
const { setRange, openPopover } = useEditorInlineBlockState('formula'); const { setRange, openPopover } = useEditorInlineBlockState('formula');
const onClick = useCallback(() => { const onClick = useCallback(() => {
const selection = editor.selection; let selection = editor.selection;
if (!selection) return; if (!selection) return;
if (formulaMatch) {
selection = editor.range(formulaMatch[1]);
editor.select(selection);
} else {
CustomEditor.toggleFormula(editor); CustomEditor.toggleFormula(editor);
}
requestAnimationFrame(() => { requestAnimationFrame(() => {
if (!selection) return;
setRange(selection); setRange(selection);
openPopover(); openPopover();
}); });
}, [editor, setRange, openPopover]); }, [editor, formulaMatch, setRange, openPopover]);
return ( return (
<ActionButton <ActionButton

View File

@ -4,7 +4,7 @@ import { generateId } from '$app/components/editor/provider/utils/convert';
import { YDelta2Delta } from '$app/components/editor/provider/utils/delta'; import { YDelta2Delta } from '$app/components/editor/provider/utils/delta';
import { YDelta } from '$app/components/editor/provider/types/y_event'; import { YDelta } from '$app/components/editor/provider/types/y_event';
import { getInsertTarget, getYTarget } from '$app/components/editor/provider/utils/relation'; import { getInsertTarget, getYTarget } from '$app/components/editor/provider/utils/relation';
import { EditorNodeType } from '$app/application/document/document.types'; import { EditorInlineNodeType, EditorNodeType } from '$app/application/document/document.types';
import { Log } from '$app/utils/log'; import { Log } from '$app/utils/log';
export function YEvents2BlockActions( export function YEvents2BlockActions(
@ -36,6 +36,30 @@ export function YEvent2BlockActions(
const backupTarget = getYTarget(backupDoc, path) as Readonly<Y.XmlText>; const backupTarget = getYTarget(backupDoc, path) as Readonly<Y.XmlText>;
const actions = []; const actions = [];
if ([EditorInlineNodeType.Formula, EditorInlineNodeType.Mention].includes(yXmlText.getAttribute('type'))) {
const parentYXmlText = yXmlText.parent as Y.XmlText;
const parentDelta = parentYXmlText.toDelta() as YDelta;
const index = parentDelta.findIndex((op) => op.insert === yXmlText);
const ops = YDelta2Delta(parentDelta);
const retainIndex = ops.reduce((acc, op, currentIndex) => {
if (currentIndex < index) {
return acc + (op.insert as string).length ?? 0;
}
return acc;
}, 0);
const newDelta = [
{
retain: retainIndex,
},
...delta,
];
actions.push(...generateApplyTextActions(parentYXmlText, newDelta));
}
if (yXmlText.getAttribute('type') === 'text') { if (yXmlText.getAttribute('type') === 'text') {
actions.push(...textOps2BlockActions(rootId, yXmlText, delta)); actions.push(...textOps2BlockActions(rootId, yXmlText, delta));
} }

View File

@ -11,7 +11,7 @@ export function transformToInlineElement(op: Op): Element | null {
const attributes = op.attributes; const attributes = op.attributes;
if (!attributes) return null; if (!attributes) return null;
const formula = attributes.formula as string; const { formula, mention, ...attrs } = attributes;
if (formula) { if (formula) {
return { return {
@ -20,23 +20,23 @@ export function transformToInlineElement(op: Op): Element | null {
children: [ children: [
{ {
text: op.insert as string, text: op.insert as string,
...attrs,
}, },
], ],
}; };
} }
const matchMention = attributes.mention as Mention; if (mention) {
if (matchMention) {
return { return {
type: EditorInlineNodeType.Mention, type: EditorInlineNodeType.Mention,
children: [ children: [
{ {
text: op.insert as string, text: op.insert as string,
...attrs,
}, },
], ],
data: { data: {
...matchMention, ...(mention as Mention),
}, },
}; };
} }

View File

@ -1,5 +1,5 @@
import { createContext, useCallback, useContext, useMemo } from 'react'; import { createContext, useCallback, useContext, useMemo } from 'react';
import { BaseRange } from 'slate'; import { BaseRange, Path } from 'slate';
import { proxy, useSnapshot } from 'valtio'; import { proxy, useSnapshot } from 'valtio';
export interface EditorInlineBlockState { export interface EditorInlineBlockState {
@ -43,8 +43,10 @@ export function useEditorInlineBlockState(key: 'formula') {
}, [context, key]); }, [context, key]);
const setRange = useCallback( const setRange = useCallback(
(range: BaseRange) => { (at: BaseRange | Path) => {
context[key].range = range; const range = Path.isPath(at) ? { anchor: at, focus: at } : at;
context[key].range = range as BaseRange;
}, },
[context, key] [context, key]
); );

View File

@ -70,12 +70,12 @@
"copyLink": "نسخ الرابط" "copyLink": "نسخ الرابط"
}, },
"moreAction": { "moreAction": {
"fontSize": "حجم الخط",
"import": "استيراد",
"moreOptions": "المزيد من الخيارات",
"small": "صغير", "small": "صغير",
"medium": "متوسط", "medium": "متوسط",
"large": "كبير" "large": "كبير",
"fontSize": "حجم الخط",
"import": "استيراد",
"moreOptions": "المزيد من الخيارات"
}, },
"importPanel": { "importPanel": {
"textAndMarkdown": "نص و Markdown", "textAndMarkdown": "نص و Markdown",

View File

@ -69,12 +69,12 @@
"copyLink": "Copiar l'enllaç" "copyLink": "Copiar l'enllaç"
}, },
"moreAction": { "moreAction": {
"fontSize": "Mida de la font",
"import": "Importar",
"moreOptions": "Més opcions",
"small": "petit", "small": "petit",
"medium": "mitjà", "medium": "mitjà",
"large": "gran" "large": "gran",
"fontSize": "Mida de la font",
"import": "Importar",
"moreOptions": "Més opcions"
}, },
"importPanel": { "importPanel": {
"textAndMarkdown": "Text i rebaixa", "textAndMarkdown": "Text i rebaixa",

View File

@ -69,12 +69,12 @@
"copyLink": "Link kopieren" "copyLink": "Link kopieren"
}, },
"moreAction": { "moreAction": {
"fontSize": "Schriftgröße",
"import": "Importieren",
"moreOptions": "Mehr Optionen",
"small": "klein", "small": "klein",
"medium": "mittel", "medium": "mittel",
"large": "groß" "large": "groß",
"fontSize": "Schriftgröße",
"import": "Importieren",
"moreOptions": "Mehr Optionen"
}, },
"importPanel": { "importPanel": {
"textAndMarkdown": "Text & Markdown", "textAndMarkdown": "Text & Markdown",

View File

@ -394,7 +394,8 @@
"twelveHour": "Twelve hour", "twelveHour": "Twelve hour",
"twentyFourHour": "Twenty four hour" "twentyFourHour": "Twenty four hour"
}, },
"showNamingDialogWhenCreatingPage": "Show naming dialog when creating a page" "showNamingDialogWhenCreatingPage": "Show naming dialog when creating a page",
"enableRTLToolbarItems": "Enable RTL toolbar items"
}, },
"files": { "files": {
"copy": "Copy", "copy": "Copy",
@ -550,7 +551,15 @@
"onOrAfter": "Is on or after", "onOrAfter": "Is on or after",
"between": "Is between", "between": "Is between",
"empty": "Is empty", "empty": "Is empty",
"notEmpty": "Is not empty" "notEmpty": "Is not empty",
"choicechipPrefix": {
"before": "Before",
"after": "After",
"onOrBefore": "On or before",
"onOrAfter": "On or after",
"isEmpty": "Is empty",
"isNotEmpty": "Is not empty"
}
}, },
"numberFilter": { "numberFilter": {
"equal": "Equals", "equal": "Equals",
@ -932,7 +941,6 @@
"addToColumnBottomTooltip": "Add a new card at the bottom", "addToColumnBottomTooltip": "Add a new card at the bottom",
"renameColumn": "Rename", "renameColumn": "Rename",
"hideColumn": "Hide", "hideColumn": "Hide",
"groupActions": "Group Actions",
"newGroup": "New Group", "newGroup": "New Group",
"deleteColumn": "Delete", "deleteColumn": "Delete",
"deleteColumnConfirmation": "This will delete this group and all the cards in it.\nAre you sure you want to continue?" "deleteColumnConfirmation": "This will delete this group and all the cards in it.\nAre you sure you want to continue?"

View File

@ -72,12 +72,12 @@
"copyLink": "Copiar enlace" "copyLink": "Copiar enlace"
}, },
"moreAction": { "moreAction": {
"fontSize": "Tamaño de fuente",
"import": "Importar",
"moreOptions": "Mas opciones",
"small": "pequeño", "small": "pequeño",
"medium": "medio", "medium": "medio",
"large": "grande" "large": "grande",
"fontSize": "Tamaño de fuente",
"import": "Importar",
"moreOptions": "Mas opciones"
}, },
"importPanel": { "importPanel": {
"textAndMarkdown": "Texto y descuento", "textAndMarkdown": "Texto y descuento",

View File

@ -50,12 +50,12 @@
"copyLink": "Esteka kopiatu" "copyLink": "Esteka kopiatu"
}, },
"moreAction": { "moreAction": {
"fontSize": "Letra tamaina",
"import": "Inportatu",
"moreOptions": "Aukera gehiago",
"small": "txikia", "small": "txikia",
"medium": "ertaina", "medium": "ertaina",
"large": "handia" "large": "handia",
"fontSize": "Letra tamaina",
"import": "Inportatu",
"moreOptions": "Aukera gehiago"
}, },
"importPanel": { "importPanel": {
"textAndMarkdown": "Testua eta Markdown", "textAndMarkdown": "Testua eta Markdown",
@ -159,7 +159,9 @@
"editContact": "Kontaktua editatu" "editContact": "Kontaktua editatu"
}, },
"button": { "button": {
"ok": "OK",
"done": "Eginda", "done": "Eginda",
"cancel": "Ezteztatu",
"signIn": "Saioa hasi", "signIn": "Saioa hasi",
"signOut": "Saioa itxi", "signOut": "Saioa itxi",
"complete": "Burututa", "complete": "Burututa",
@ -175,9 +177,7 @@
"edit": "Editatu", "edit": "Editatu",
"delete": "Ezabatu", "delete": "Ezabatu",
"duplicate": "Bikoiztu", "duplicate": "Bikoiztu",
"putback": "Jarri Atzera", "putback": "Jarri Atzera"
"cancel": "Ezteztatu",
"ok": "OK"
}, },
"label": { "label": {
"welcome": "Ongi etorri!", "welcome": "Ongi etorri!",

View File

@ -55,12 +55,12 @@
"copyLink": "کپی کردن لینک" "copyLink": "کپی کردن لینک"
}, },
"moreAction": { "moreAction": {
"fontSize": "اندازه قلم",
"import": "اضافه کردن",
"moreOptions": "گزینه های بیشتر",
"small": "کوچک", "small": "کوچک",
"medium": "متوسط", "medium": "متوسط",
"large": "بزرگ" "large": "بزرگ",
"fontSize": "اندازه قلم",
"import": "اضافه کردن",
"moreOptions": "گزینه های بیشتر"
}, },
"importPanel": { "importPanel": {
"textAndMarkdown": "Text & Markdown", "textAndMarkdown": "Text & Markdown",
@ -175,7 +175,9 @@
"editContact": "ویرایش مخاطب" "editContact": "ویرایش مخاطب"
}, },
"button": { "button": {
"ok": "باشه",
"done": "انجام شد", "done": "انجام شد",
"cancel": "لغو",
"signIn": "ورود", "signIn": "ورود",
"signOut": "خروج", "signOut": "خروج",
"complete": "کامل شد", "complete": "کامل شد",
@ -191,9 +193,7 @@
"edit": "ویرایش", "edit": "ویرایش",
"delete": "حذف کردن", "delete": "حذف کردن",
"duplicate": "تکرار کردن", "duplicate": "تکرار کردن",
"putback": "بازگشت", "putback": "بازگشت"
"cancel": "لغو",
"ok": "باشه"
}, },
"label": { "label": {
"welcome": "خوش آمدید!", "welcome": "خوش آمدید!",

View File

@ -50,12 +50,12 @@
"copyLink": "Link másolása" "copyLink": "Link másolása"
}, },
"moreAction": { "moreAction": {
"fontSize": "Betűméret",
"import": "Importálás",
"moreOptions": "Több lehetőség",
"small": "kicsi", "small": "kicsi",
"medium": "közepes", "medium": "közepes",
"large": "nagy" "large": "nagy",
"fontSize": "Betűméret",
"import": "Importálás",
"moreOptions": "Több lehetőség"
}, },
"importPanel": { "importPanel": {
"textAndMarkdown": "Szöveg & Markdown", "textAndMarkdown": "Szöveg & Markdown",
@ -163,7 +163,9 @@
"editContact": "Kontakt Szerkesztése" "editContact": "Kontakt Szerkesztése"
}, },
"button": { "button": {
"ok": "OK",
"done": "Kész", "done": "Kész",
"cancel": "Mégse",
"signIn": "Bejelentkezés", "signIn": "Bejelentkezés",
"signOut": "Kijelentkezés", "signOut": "Kijelentkezés",
"complete": "Kész", "complete": "Kész",
@ -179,9 +181,7 @@
"edit": "Szerkesztés", "edit": "Szerkesztés",
"delete": "Töröl", "delete": "Töröl",
"duplicate": "Másolat", "duplicate": "Másolat",
"putback": "Visszatesz", "putback": "Visszatesz"
"cancel": "Mégse",
"ok": "OK"
}, },
"label": { "label": {
"welcome": "Üdvözlünk!", "welcome": "Üdvözlünk!",

View File

@ -72,12 +72,12 @@
"copyLink": "Salin tautan" "copyLink": "Salin tautan"
}, },
"moreAction": { "moreAction": {
"fontSize": "Ukuran huruf",
"import": "Impor",
"moreOptions": "Lebih banyak pilihan",
"small": "kecil", "small": "kecil",
"medium": "sedang", "medium": "sedang",
"large": "besar" "large": "besar",
"fontSize": "Ukuran huruf",
"import": "Impor",
"moreOptions": "Lebih banyak pilihan"
}, },
"importPanel": { "importPanel": {
"textAndMarkdown": "Teks & Markdown", "textAndMarkdown": "Teks & Markdown",

View File

@ -59,6 +59,8 @@
"failedToLoad": "Qualcosa è andato storto! Impossibile caricare lo spazio di lavoro. Prova a chiudere qualsiasi istanza aperta di AppFlowy e riprova.", "failedToLoad": "Qualcosa è andato storto! Impossibile caricare lo spazio di lavoro. Prova a chiudere qualsiasi istanza aperta di AppFlowy e riprova.",
"errorActions": { "errorActions": {
"reportIssue": "Segnala un problema", "reportIssue": "Segnala un problema",
"reportIssueOnGithub": "Segnalate un problema su Github",
"exportLogFiles": "Esporta i file di log",
"reachOut": "Contattaci su Discord" "reachOut": "Contattaci su Discord"
} }
}, },
@ -70,12 +72,16 @@
"copyLink": "Copia Link" "copyLink": "Copia Link"
}, },
"moreAction": { "moreAction": {
"small": "piccolo",
"medium": "medio",
"large": "grande",
"fontSize": "Dimensione del font", "fontSize": "Dimensione del font",
"import": "Importare", "import": "Importare",
"moreOptions": "Più opzioni", "moreOptions": "Più opzioni",
"small": "piccolo", "wordCount": "Conteggio parole: {}",
"medium": "medio", "charCount": "Numero di caratteri: {}",
"large": "grande" "deleteView": "Cancella",
"duplicateView": "Duplica"
}, },
"importPanel": { "importPanel": {
"textAndMarkdown": "Testo e markdown", "textAndMarkdown": "Testo e markdown",
@ -231,7 +237,11 @@
"rename": "Rinomina", "rename": "Rinomina",
"helpCenter": "Centro assistenza", "helpCenter": "Centro assistenza",
"add": "Aggiungi", "add": "Aggiungi",
"yes": "SÌ" "yes": "SÌ",
"Done": "Fatto",
"Cancel": "Annulla",
"remove": "Rimuovi",
"dontRemove": "Non rimuovere"
}, },
"label": { "label": {
"welcome": "Benvenuto!", "welcome": "Benvenuto!",
@ -276,7 +286,10 @@
"cloudLocal": "Locale", "cloudLocal": "Locale",
"cloudSupabase": "Supabase", "cloudSupabase": "Supabase",
"cloudSupabaseUrl": "URL di Supabase", "cloudSupabaseUrl": "URL di Supabase",
"cloudSupabaseUrlCanNotBeEmpty": "L'url di supabase non può essere vuoto",
"cloudAppFlowy": "AppFlowy Cloud", "cloudAppFlowy": "AppFlowy Cloud",
"cloudAppFlowySelfHost": "AppFlowy Cloud Self-hosted (autogestito)",
"appFlowyCloudUrlCanNotBeEmpty": "L'url del cloud non può essere vuoto",
"clickToCopy": "Fare clic per copiare", "clickToCopy": "Fare clic per copiare",
"selfHostStart": "Se non disponi di un server, fai riferimento a", "selfHostStart": "Se non disponi di un server, fai riferimento a",
"selfHostContent": "documento", "selfHostContent": "documento",
@ -286,6 +299,7 @@
"cloudWSURLHint": "Inserisci l'indirizzo websocket del tuo server", "cloudWSURLHint": "Inserisci l'indirizzo websocket del tuo server",
"restartApp": "Riavvia", "restartApp": "Riavvia",
"restartAppTip": "Riavvia l'applicazione affinché le modifiche abbiano effetto. Tieni presente che ciò potrebbe disconnettere il tuo account corrente", "restartAppTip": "Riavvia l'applicazione affinché le modifiche abbiano effetto. Tieni presente che ciò potrebbe disconnettere il tuo account corrente",
"changeServerTip": "Dopo aver modificato il server, è necessario fare clic sul pulsante di riavvio affinché le modifiche abbiano effetto.",
"enableEncryptPrompt": "Attiva la crittografia per proteggere i tuoi dati con questo segreto. Conservarlo in modo sicuro; una volta abilitato, non può essere spento. In caso di perdita, i tuoi dati diventano irrecuperabili. Fare clic per copiare", "enableEncryptPrompt": "Attiva la crittografia per proteggere i tuoi dati con questo segreto. Conservarlo in modo sicuro; una volta abilitato, non può essere spento. In caso di perdita, i tuoi dati diventano irrecuperabili. Fare clic per copiare",
"inputEncryptPrompt": "Inserisci il tuo segreto di crittografia per", "inputEncryptPrompt": "Inserisci il tuo segreto di crittografia per",
"clickToCopySecret": "Fare clic per copiare il segreto", "clickToCopySecret": "Fare clic per copiare il segreto",
@ -297,6 +311,7 @@
"openHistoricalUser": "Fare clic per aprire l'account anonimo", "openHistoricalUser": "Fare clic per aprire l'account anonimo",
"customPathPrompt": "L'archiviazione della cartella dati di AppFlowy in una cartella sincronizzata sul cloud come Google Drive può comportare rischi. Se si accede o si modifica il database all'interno di questa cartella da più posizioni contemporaneamente, potrebbero verificarsi conflitti di sincronizzazione e potenziale danneggiamento dei dati", "customPathPrompt": "L'archiviazione della cartella dati di AppFlowy in una cartella sincronizzata sul cloud come Google Drive può comportare rischi. Se si accede o si modifica il database all'interno di questa cartella da più posizioni contemporaneamente, potrebbero verificarsi conflitti di sincronizzazione e potenziale danneggiamento dei dati",
"importAppFlowyData": "Importa dati dalla cartella AppFlowy esterna", "importAppFlowyData": "Importa dati dalla cartella AppFlowy esterna",
"importingAppFlowyDataTip": "L'importazione dei dati è in corso. Non chiudere l'applicazione",
"importAppFlowyDataDescription": "Copia i dati da una cartella dati AppFlowy esterna e importali nella cartella dati AppFlowy corrente", "importAppFlowyDataDescription": "Copia i dati da una cartella dati AppFlowy esterna e importali nella cartella dati AppFlowy corrente",
"importSuccess": "Importazione della cartella dati AppFlowy riuscita", "importSuccess": "Importazione della cartella dati AppFlowy riuscita",
"importFailed": "L'importazione della cartella dati di AppFlowy non è riuscita", "importFailed": "L'importazione della cartella dati di AppFlowy non è riuscita",
@ -441,10 +456,12 @@
"joinDiscord": "Unisciti a noi su Discord", "joinDiscord": "Unisciti a noi su Discord",
"privacyPolicy": "Politica sulla riservatezza", "privacyPolicy": "Politica sulla riservatezza",
"userAgreement": "Accordo per gli utenti", "userAgreement": "Accordo per gli utenti",
"termsAndConditions": "Termini e Condizioni",
"userprofileError": "Impossibile caricare il profilo utente", "userprofileError": "Impossibile caricare il profilo utente",
"userprofileErrorDescription": "Prova a disconnetterti e ad accedere nuovamente per verificare se il problema persiste.", "userprofileErrorDescription": "Prova a disconnetterti e ad accedere nuovamente per verificare se il problema persiste.",
"selectLayout": "Seleziona disposizione", "selectLayout": "Seleziona disposizione",
"selectStartingDay": "Seleziona il giorno di inizio" "selectStartingDay": "Seleziona il giorno di inizio",
"version": "Versione"
} }
}, },
"grid": { "grid": {
@ -466,14 +483,14 @@
"typeAValue": "Digita un valore...", "typeAValue": "Digita un valore...",
"layout": "Disposizione", "layout": "Disposizione",
"databaseLayout": "Disposizione", "databaseLayout": "Disposizione",
"viewList": "Viste del database",
"editView": "Modifica vista", "editView": "Modifica vista",
"boardSettings": "Impostazioni della bacheca", "boardSettings": "Impostazioni della bacheca",
"calendarSettings": "Impostazioni del calendario", "calendarSettings": "Impostazioni del calendario",
"createView": "Nuova vista", "createView": "Nuova vista",
"duplicateView": "Duplica vista", "duplicateView": "Duplica vista",
"deleteView": "Elimina vista", "deleteView": "Elimina vista",
"numberOfVisibleFields": "{} mostrato", "numberOfVisibleFields": "{} mostrato"
"viewList": "Viste del database"
}, },
"textFilter": { "textFilter": {
"contains": "Contiene", "contains": "Contiene",
@ -525,6 +542,16 @@
"empty": "È vuoto", "empty": "È vuoto",
"notEmpty": "Non è vuoto" "notEmpty": "Non è vuoto"
}, },
"numberFilter": {
"equal": "Uguale",
"notEqual": "Non è uguale",
"lessThan": "È meno di",
"greaterThan": "È maggiore di",
"lessThanOrEqualTo": "È inferiore o uguale a",
"greaterThanOrEqualTo": "È maggiore o uguale a",
"isEmpty": "È vuoto",
"isNotEmpty": "Non è vuoto"
},
"field": { "field": {
"hide": "Nascondere", "hide": "Nascondere",
"show": "Mostra", "show": "Mostra",
@ -556,6 +583,7 @@
"timeFormatTwelveHour": "12 ore", "timeFormatTwelveHour": "12 ore",
"timeFormatTwentyFourHour": "24 ore", "timeFormatTwentyFourHour": "24 ore",
"clearDate": "Pulisci data", "clearDate": "Pulisci data",
"dateTime": "Data e ora",
"failedToLoadDate": "Impossibile caricare il valore della data", "failedToLoadDate": "Impossibile caricare il valore della data",
"selectTime": "Seleziona l'ora", "selectTime": "Seleziona l'ora",
"selectDate": "Seleziona la data", "selectDate": "Seleziona la data",
@ -569,7 +597,9 @@
"newProperty": "Nuova proprietà", "newProperty": "Nuova proprietà",
"deleteFieldPromptMessage": "Sei sicuro? Questa proprietà verrà eliminata", "deleteFieldPromptMessage": "Sei sicuro? Questa proprietà verrà eliminata",
"newColumn": "Nuova colonna", "newColumn": "Nuova colonna",
"format": "Formato" "format": "Formato",
"reminderOnDateTooltip": "Questa cella ha un promemoria programmato",
"optionAlreadyExist": "L'opzione esiste già"
}, },
"rowPage": { "rowPage": {
"newField": "Aggiungi un nuovo campo", "newField": "Aggiungi un nuovo campo",
@ -588,6 +618,7 @@
"sort": { "sort": {
"ascending": "Ascendente", "ascending": "Ascendente",
"descending": "Discendente", "descending": "Discendente",
"cannotFindCreatableField": "Impossibile trovare un campo adatto per l'ordinamento",
"deleteAllSorts": "Elimina tutti gli ordinamenti", "deleteAllSorts": "Elimina tutti gli ordinamenti",
"addSort": "Aggiungi ordinamento", "addSort": "Aggiungi ordinamento",
"deleteSort": "Elimina ordinamento" "deleteSort": "Elimina ordinamento"
@ -636,7 +667,16 @@
"showComplete": "Mostra tutte le attività" "showComplete": "Mostra tutte le attività"
}, },
"menuName": "Griglia", "menuName": "Griglia",
"referencedGridPrefix": "Vista di" "referencedGridPrefix": "Vista di",
"calculate": "Calcolare",
"calculationTypeLabel": {
"average": "Media",
"max": "Massimo",
"median": "Medio",
"min": "Minimo",
"sum": "Somma"
},
"removeSorting": "Si desidera rimuovere l'ordinamento?"
}, },
"document": { "document": {
"menuName": "Documento", "menuName": "Documento",
@ -742,13 +782,16 @@
}, },
"image": { "image": {
"copiedToPasteBoard": "Il link dell'immagine è stato copiato negli appunti", "copiedToPasteBoard": "Il link dell'immagine è stato copiato negli appunti",
"addAnImage": "Aggiungi un'immagine" "addAnImage": "Aggiungi un'immagine",
"imageUploadFailed": "Caricamento dell'immagine non riuscito"
}, },
"urlPreview": { "urlPreview": {
"copiedToPasteBoard": "Il link è stato copiato negli appunti" "copiedToPasteBoard": "Il link è stato copiato negli appunti",
"convertToLink": "Convertire in link da incorporare"
}, },
"outline": { "outline": {
"addHeadingToCreateOutline": "Aggiungi intestazioni per creare un sommario." "addHeadingToCreateOutline": "Aggiungi intestazioni per creare un sommario.",
"noMatchHeadings": "Non sono stati trovati titoli corrispondenti."
}, },
"table": { "table": {
"addAfter": "Aggiungi dopo", "addAfter": "Aggiungi dopo",
@ -771,7 +814,9 @@
"toContinue": "continuare", "toContinue": "continuare",
"newDatabase": "Nuova banca dati", "newDatabase": "Nuova banca dati",
"linkToDatabase": "Collegamento alla banca dati" "linkToDatabase": "Collegamento alla banca dati"
} },
"date": "Data",
"emoji": "Emoji"
}, },
"textBlock": { "textBlock": {
"placeholder": "Digita '/' per i comandi" "placeholder": "Digita '/' per i comandi"
@ -817,7 +862,10 @@
"saveImageToGallery": "Salva immagine", "saveImageToGallery": "Salva immagine",
"failedToAddImageToGallery": "Impossibile aggiungere l'immagine alla galleria", "failedToAddImageToGallery": "Impossibile aggiungere l'immagine alla galleria",
"successToAddImageToGallery": "Immagine aggiunta alla galleria con successo", "successToAddImageToGallery": "Immagine aggiunta alla galleria con successo",
"unableToLoadImage": "Impossibile caricare l'immagine" "unableToLoadImage": "Impossibile caricare l'immagine",
"maximumImageSize": "La dimensione massima supportata per il caricamento delle immagini è di 10 MB",
"uploadImageErrorImageSizeTooBig": "Le dimensioni dell'immagine devono essere inferiori a 10 MB",
"imageIsUploading": "L'immagine si sta caricando"
}, },
"codeBlock": { "codeBlock": {
"language": { "language": {
@ -844,7 +892,9 @@
"page": { "page": {
"label": "Collegamento alla pagina", "label": "Collegamento alla pagina",
"tooltip": "Fare clic per aprire la pagina" "tooltip": "Fare clic per aprire la pagina"
} },
"deleted": "Eliminato",
"deletedContent": "Questo contenuto non esiste o è stato cancellato"
}, },
"toolbar": { "toolbar": {
"resetToDefaultFont": "Ripristina alle condizioni predefinite" "resetToDefaultFont": "Ripristina alle condizioni predefinite"
@ -876,6 +926,7 @@
"cardDuplicated": "La carta è stata duplicata", "cardDuplicated": "La carta è stata duplicata",
"cardDeleted": "La carta è stata eliminata", "cardDeleted": "La carta è stata eliminata",
"showOnCard": "Mostra sui dettagli della carta", "showOnCard": "Mostra sui dettagli della carta",
"setting": "Impostazioni",
"propertyName": "Nome della proprietà", "propertyName": "Nome della proprietà",
"menuName": "Bacheca", "menuName": "Bacheca",
"showUngrouped": "Mostra elementi non raggruppati", "showUngrouped": "Mostra elementi non raggruppati",
@ -902,16 +953,21 @@
"previousMonth": "Il mese scorso", "previousMonth": "Il mese scorso",
"nextMonth": "Il prossimo mese" "nextMonth": "Il prossimo mese"
}, },
"mobileEventScreen": {
"emptyTitle": "Non ci sono ancora eventi",
"emptyBody": "Premere il pulsante più per creare un evento in questo giorno."
},
"settings": { "settings": {
"showWeekNumbers": "Mostra i numeri della settimana", "showWeekNumbers": "Mostra i numeri della settimana",
"showWeekends": "Mostra i fine settimana", "showWeekends": "Mostra i fine settimana",
"firstDayOfWeek": "Inizia la settimana", "firstDayOfWeek": "Inizia la settimana",
"layoutDateField": "Layout calendario per", "layoutDateField": "Layout calendario per",
"changeLayoutDateField": "Modifica del campo di layout",
"noDateTitle": "Nessuna data", "noDateTitle": "Nessuna data",
"noDateHint": "Gli eventi non programmati verranno visualizzati qui",
"unscheduledEventsTitle": "Eventi non programmati", "unscheduledEventsTitle": "Eventi non programmati",
"clickToAdd": "Fare clic per aggiungere al calendario", "clickToAdd": "Fare clic per aggiungere al calendario",
"name": "Disposizione del calendario", "name": "Disposizione del calendario"
"noDateHint": "Gli eventi non programmati verranno visualizzati qui"
}, },
"referencedCalendarPrefix": "Vista di", "referencedCalendarPrefix": "Vista di",
"quickJumpYear": "Salta a" "quickJumpYear": "Salta a"
@ -983,9 +1039,14 @@
}, },
"inlineActions": { "inlineActions": {
"noResults": "Nessun risultato", "noResults": "Nessun risultato",
"pageReference": "Riferimento della pagina",
"docReference": "Riferimento al documento",
"calReference": "Calendario di riferimento",
"gridReference": "Riferimento griglia",
"date": "Data", "date": "Data",
"reminder": { "reminder": {
"groupTitle": "Promemoria" "groupTitle": "Promemoria",
"shortKeyword": "ricordare"
} }
}, },
"datePicker": { "datePicker": {
@ -994,7 +1055,23 @@
"includeTime": "Includere l'orario", "includeTime": "Includere l'orario",
"isRange": "Data di fine", "isRange": "Data di fine",
"timeFormat": "Formato orario", "timeFormat": "Formato orario",
"clearDate": "Rimuovi data" "clearDate": "Rimuovi data",
"reminderLabel": "Promemoria",
"selectReminder": "Seleziona il promemoria",
"reminderOptions": {
"atTimeOfEvent": "Ora dell'evento",
"fiveMinsBefore": "5 minuti prima",
"tenMinsBefore": "10 minuti prima",
"fifteenMinsBefore": "15 minuti prima",
"thirtyMinsBefore": "30 minuti prima",
"oneHourBefore": "1 ora prima",
"twoHoursBefore": "2 hours before",
"onDayOfEvent": "Il giorno dell'evento",
"oneDayBefore": "1 giorno prima",
"twoDaysBefore": "2 giorni prima",
"oneWeekBefore": "1 settimana prima",
"custom": "Personalizzato"
}
}, },
"relativeDates": { "relativeDates": {
"yesterday": "Ieri", "yesterday": "Ieri",
@ -1010,6 +1087,7 @@
"emptyTitle": "Tutto a posto!", "emptyTitle": "Tutto a posto!",
"emptyBody": "Nessuna notifica o azione in sospeso. Goditi la calma.", "emptyBody": "Nessuna notifica o azione in sospeso. Goditi la calma.",
"tabs": { "tabs": {
"inbox": "Posta in arrivo",
"upcoming": "Prossimamente" "upcoming": "Prossimamente"
}, },
"actions": { "actions": {
@ -1057,6 +1135,7 @@
"highlight": "Evidenzia", "highlight": "Evidenzia",
"color": "Colore", "color": "Colore",
"image": "Immagine", "image": "Immagine",
"date": "Data",
"italic": "Corsivo", "italic": "Corsivo",
"link": "Link", "link": "Link",
"numberedList": "Elenco numerato", "numberedList": "Elenco numerato",
@ -1158,7 +1237,10 @@
"colClear": "Rimuovi contenuto", "colClear": "Rimuovi contenuto",
"rowClear": "Rimuovi contenuto", "rowClear": "Rimuovi contenuto",
"slashPlaceHolder": "Digita \"/\" per inserire un blocco o inizia a digitare", "slashPlaceHolder": "Digita \"/\" per inserire un blocco o inizia a digitare",
"typeSomething": "Scrivi qualcosa..." "typeSomething": "Scrivi qualcosa...",
"quoteListShortForm": "Citazione",
"mathEquationShortForm": "Formula",
"codeBlockShortForm": "Codice"
}, },
"favorite": { "favorite": {
"noFavorite": "Nessuna pagina preferita", "noFavorite": "Nessuna pagina preferita",
@ -1182,5 +1264,6 @@
"date": "Data", "date": "Data",
"addField": "Aggiungi campo", "addField": "Aggiungi campo",
"userIcon": "Icona utente" "userIcon": "Icona utente"
} },
"noLogFiles": "Non ci sono file di log"
} }

View File

@ -63,12 +63,12 @@
"copyLink": "リンクをコピー" "copyLink": "リンクをコピー"
}, },
"moreAction": { "moreAction": {
"fontSize": "フォントサイズ",
"import": "取り込む",
"moreOptions": "より多くのオプション",
"small": "小さい", "small": "小さい",
"medium": "中くらい", "medium": "中くらい",
"large": "大きい" "large": "大きい",
"fontSize": "フォントサイズ",
"import": "取り込む",
"moreOptions": "より多くのオプション"
}, },
"importPanel": { "importPanel": {
"textAndMarkdown": "テキストとマークダウン", "textAndMarkdown": "テキストとマークダウン",

View File

@ -50,12 +50,12 @@
"copyLink": "링크 복사" "copyLink": "링크 복사"
}, },
"moreAction": { "moreAction": {
"fontSize": "글꼴 크기",
"import": "수입",
"moreOptions": "추가 옵션",
"small": "작은", "small": "작은",
"medium": "중간", "medium": "중간",
"large": "크기가 큰" "large": "크기가 큰",
"fontSize": "글꼴 크기",
"import": "수입",
"moreOptions": "추가 옵션"
}, },
"importPanel": { "importPanel": {
"textAndMarkdown": "텍스트 및 마크다운", "textAndMarkdown": "텍스트 및 마크다운",
@ -159,7 +159,9 @@
"editContact": "연락처 편집" "editContact": "연락처 편집"
}, },
"button": { "button": {
"ok": "확인",
"done": "완료", "done": "완료",
"cancel": "취소",
"signIn": "로그인", "signIn": "로그인",
"signOut": "로그아웃", "signOut": "로그아웃",
"complete": "완료", "complete": "완료",
@ -175,9 +177,7 @@
"edit": "편집하다", "edit": "편집하다",
"delete": "삭제", "delete": "삭제",
"duplicate": "복제하다", "duplicate": "복제하다",
"putback": "다시 집어 넣어", "putback": "다시 집어 넣어"
"cancel": "취소",
"ok": "확인"
}, },
"label": { "label": {
"welcome": "환영합니다!", "welcome": "환영합니다!",

View File

@ -70,12 +70,12 @@
"copyLink": "Skopiuj link" "copyLink": "Skopiuj link"
}, },
"moreAction": { "moreAction": {
"fontSize": "Rozmiar czcionki",
"import": "Import",
"moreOptions": "Więcej opcji",
"small": "mały", "small": "mały",
"medium": "średni", "medium": "średni",
"large": "duży" "large": "duży",
"fontSize": "Rozmiar czcionki",
"import": "Import",
"moreOptions": "Więcej opcji"
}, },
"importPanel": { "importPanel": {
"textAndMarkdown": "Tekst i Markdown", "textAndMarkdown": "Tekst i Markdown",

View File

@ -72,12 +72,12 @@
"copyLink": "Copiar link" "copyLink": "Copiar link"
}, },
"moreAction": { "moreAction": {
"fontSize": "Tamanho da fonte",
"import": "Importar",
"moreOptions": "Mais opções",
"small": "pequeno", "small": "pequeno",
"medium": "médio", "medium": "médio",
"large": "grande" "large": "grande",
"fontSize": "Tamanho da fonte",
"import": "Importar",
"moreOptions": "Mais opções"
}, },
"importPanel": { "importPanel": {
"textAndMarkdown": "Texto e Remarcação", "textAndMarkdown": "Texto e Remarcação",

View File

@ -70,12 +70,12 @@
"copyLink": "Copiar o link" "copyLink": "Copiar o link"
}, },
"moreAction": { "moreAction": {
"fontSize": "Tamanho da fonte",
"import": "Importar",
"moreOptions": "Mais opções",
"small": "pequeno", "small": "pequeno",
"medium": "médio", "medium": "médio",
"large": "grande" "large": "grande",
"fontSize": "Tamanho da fonte",
"import": "Importar",
"moreOptions": "Mais opções"
}, },
"importPanel": { "importPanel": {
"textAndMarkdown": "Texto e Remarcação", "textAndMarkdown": "Texto e Remarcação",
@ -194,7 +194,9 @@
"editContact": "Editar um conctato" "editContact": "Editar um conctato"
}, },
"button": { "button": {
"ok": "OK",
"done": "Feito", "done": "Feito",
"cancel": "Cancelar",
"signIn": "Entrar", "signIn": "Entrar",
"signOut": "Sair", "signOut": "Sair",
"complete": "Completar", "complete": "Completar",
@ -210,9 +212,7 @@
"edit": "Editar", "edit": "Editar",
"delete": "Excluir", "delete": "Excluir",
"duplicate": "Duplicado", "duplicate": "Duplicado",
"putback": "Por de volta", "putback": "Por de volta"
"cancel": "Cancelar",
"ok": "OK"
}, },
"label": { "label": {
"welcome": "Bem vindo!", "welcome": "Bem vindo!",

View File

@ -70,12 +70,12 @@
"copyLink": "Скопировать ссылку" "copyLink": "Скопировать ссылку"
}, },
"moreAction": { "moreAction": {
"fontSize": "Размер шрифта",
"import": "Импорт",
"moreOptions": "Дополнительные опции",
"small": "маленький", "small": "маленький",
"medium": "средний", "medium": "средний",
"large": "большой" "large": "большой",
"fontSize": "Размер шрифта",
"import": "Импорт",
"moreOptions": "Дополнительные опции"
}, },
"importPanel": { "importPanel": {
"textAndMarkdown": "Текст и Markdown", "textAndMarkdown": "Текст и Markdown",

View File

@ -69,12 +69,12 @@
"copyLink": "Bağlantıyı kopyala" "copyLink": "Bağlantıyı kopyala"
}, },
"moreAction": { "moreAction": {
"fontSize": "Yazı boyutu",
"import": "İçe aktar",
"moreOptions": "Diğer seçenekler",
"small": "Küçük", "small": "Küçük",
"medium": "Orta", "medium": "Orta",
"large": "Büyük" "large": "Büyük",
"fontSize": "Yazı boyutu",
"import": "İçe aktar",
"moreOptions": "Diğer seçenekler"
}, },
"importPanel": { "importPanel": {
"textAndMarkdown": "Metin ve Markdown", "textAndMarkdown": "Metin ve Markdown",

View File

@ -72,12 +72,12 @@
"copyLink": "Sao chép đường dẫn" "copyLink": "Sao chép đường dẫn"
}, },
"moreAction": { "moreAction": {
"fontSize": "Cỡ chữ",
"import": "Import",
"moreOptions": "Lựa chọn khác",
"small": "nhỏ", "small": "nhỏ",
"medium": "trung bình", "medium": "trung bình",
"large": "lớn" "large": "lớn",
"fontSize": "Cỡ chữ",
"import": "Import",
"moreOptions": "Lựa chọn khác"
}, },
"importPanel": { "importPanel": {
"textAndMarkdown": "Văn bản &amp; Markdown", "textAndMarkdown": "Văn bản &amp; Markdown",

View File

@ -74,12 +74,12 @@
"copyLink": "复制链接" "copyLink": "复制链接"
}, },
"moreAction": { "moreAction": {
"fontSize": "字体大小",
"import": "导入",
"moreOptions": "更多选项",
"small": "小", "small": "小",
"medium": "中", "medium": "中",
"large": "大" "large": "大",
"fontSize": "字体大小",
"import": "导入",
"moreOptions": "更多选项"
}, },
"importPanel": { "importPanel": {
"textAndMarkdown": "文本和Markdown", "textAndMarkdown": "文本和Markdown",

View File

@ -73,12 +73,12 @@
"copyLink": "複製連結" "copyLink": "複製連結"
}, },
"moreAction": { "moreAction": {
"fontSize": "字型大小",
"import": "匯入",
"moreOptions": "更多選項",
"small": "小", "small": "小",
"medium": "中", "medium": "中",
"large": "大" "large": "大",
"fontSize": "字型大小",
"import": "匯入",
"moreOptions": "更多選項"
}, },
"importPanel": { "importPanel": {
"textAndMarkdown": "文字 & Markdown", "textAndMarkdown": "文字 & Markdown",

View File

@ -73,6 +73,7 @@ impl TypeOptionTransform for RichTextTypeOption {
|| transformed_field_type.is_multi_select() || transformed_field_type.is_multi_select()
|| transformed_field_type.is_number() || transformed_field_type.is_number()
|| transformed_field_type.is_url() || transformed_field_type.is_url()
|| transformed_field_type.is_checklist()
{ {
Some(StrCellData::from(stringify_cell_data( Some(StrCellData::from(stringify_cell_data(
cell, cell,

View File

@ -206,7 +206,7 @@ async fn grid_switch_from_multi_select_to_text_test() {
// Test when switching the current field from Checkbox to Text test // Test when switching the current field from Checkbox to Text test
// input: // input:
// check -> "Yes" // check -> "Yes"
// unchecked -> "" // unchecked -> "No"
#[tokio::test] #[tokio::test]
async fn grid_switch_from_checkbox_to_text_test() { async fn grid_switch_from_checkbox_to_text_test() {
let mut test = DatabaseFieldTest::new().await; let mut test = DatabaseFieldTest::new().await;
@ -290,3 +290,24 @@ async fn grid_switch_from_number_to_text_test() {
test.run_scripts(scripts).await; test.run_scripts(scripts).await;
} }
/// Test when switching the current field from Checklist to Text test
#[tokio::test]
async fn grid_switch_from_checklist_to_text_test() {
let mut test = DatabaseFieldTest::new().await;
let field_rev = test.get_first_field(FieldType::Checklist);
let scripts = vec![
SwitchToField {
field_id: field_rev.id.clone(),
new_field_type: FieldType::RichText,
},
AssertCellContent {
field_id: field_rev.id.clone(),
row_index: 0,
from_field_type: FieldType::Checklist,
expected_content: "First thing".to_string(),
},
];
test.run_scripts(scripts).await;
}

View File

@ -68,6 +68,10 @@ pub struct AppearanceSettingsPB {
#[pb(index = 12)] #[pb(index = 12)]
#[serde(default)] #[serde(default)]
pub document_setting: DocumentSettingsPB, pub document_setting: DocumentSettingsPB,
#[pb(index = 13)]
#[serde(default)]
pub enable_rtl_toolbar_items: bool,
} }
const DEFAULT_RESET_VALUE: fn() -> bool = || APPEARANCE_RESET_AS_DEFAULT; const DEFAULT_RESET_VALUE: fn() -> bool = || APPEARANCE_RESET_AS_DEFAULT;
@ -129,6 +133,7 @@ pub const APPEARANCE_DEFAULT_MONOSPACE_FONT: &str = "SF Mono";
const APPEARANCE_RESET_AS_DEFAULT: bool = true; const APPEARANCE_RESET_AS_DEFAULT: bool = true;
const APPEARANCE_DEFAULT_IS_MENU_COLLAPSED: bool = false; const APPEARANCE_DEFAULT_IS_MENU_COLLAPSED: bool = false;
const APPEARANCE_DEFAULT_MENU_OFFSET: f64 = 0.0; const APPEARANCE_DEFAULT_MENU_OFFSET: f64 = 0.0;
const APPEARANCE_DEFAULT_ENABLE_RTL_TOOLBAR_ITEMS: bool = false;
impl std::default::Default for AppearanceSettingsPB { impl std::default::Default for AppearanceSettingsPB {
fn default() -> Self { fn default() -> Self {
@ -144,6 +149,7 @@ impl std::default::Default for AppearanceSettingsPB {
menu_offset: APPEARANCE_DEFAULT_MENU_OFFSET, menu_offset: APPEARANCE_DEFAULT_MENU_OFFSET,
layout_direction: LayoutDirectionPB::default(), layout_direction: LayoutDirectionPB::default(),
text_direction: TextDirectionPB::default(), text_direction: TextDirectionPB::default(),
enable_rtl_toolbar_items: APPEARANCE_DEFAULT_ENABLE_RTL_TOOLBAR_ITEMS,
document_setting: DocumentSettingsPB::default(), document_setting: DocumentSettingsPB::default(),
} }
} }