Kaynağa Gözat

Merge branch 'upstream-main' into feat/tauri-kanban

# Conflicts:
#	frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_bd_svc.ts
ascarbek 2 yıl önce
ebeveyn
işleme
745ee264c8
31 değiştirilmiş dosya ile 275 ekleme ve 156 silme
  1. 5 2
      frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart
  2. 4 6
      frontend/appflowy_flutter/lib/plugins/database_view/application/database_view_service.dart
  3. 2 2
      frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart
  4. 1 0
      frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/toolbar/board_toolbar.dart
  5. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart
  6. 2 2
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/date.dart
  7. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/number.dart
  8. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/select_option.dart
  9. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/sort_editor.dart
  10. 18 15
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/filter_button.dart
  11. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/grid_property.dart
  12. 19 16
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/setting_button.dart
  13. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/sort_button.dart
  14. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor.dart
  15. 3 3
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart
  16. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor.dart
  17. 2 2
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/url_cell/url_cell.dart
  18. 52 44
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/cover_image_picker_bloc.dart
  19. 1 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_button.dart
  20. 7 4
      frontend/appflowy_flutter/lib/user/application/user_listener.dart
  21. 1 0
      frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/header/add_button.dart
  22. 1 0
      frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart
  23. 3 0
      frontend/appflowy_flutter/lib/workspace/presentation/widgets/pop_up_action.dart
  24. 31 0
      frontend/appflowy_tauri/src/appflowy_app/components/tests/TestGroup.tsx
  25. 13 5
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_bd_svc.ts
  26. 1 1
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_controller.ts
  27. 2 1
      frontend/rust-lib/flowy-database/src/services/group/controller_impls/select_option_controller/util.rs
  28. 17 15
      frontend/rust-lib/flowy-folder/src/services/workspace/controller.rs
  29. 34 17
      frontend/scripts/install_dev_env/install_linux.sh
  30. 26 8
      frontend/scripts/install_dev_env/install_macos.sh
  31. 22 5
      frontend/scripts/install_dev_env/install_windows.sh

+ 5 - 2
frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart

@@ -159,8 +159,11 @@ class DatabaseController {
     );
   }
 
-  Future<Either<Unit, FlowyError>> moveRow(RowPB fromRow,
-      {RowPB? toRow, String? groupId}) {
+  Future<Either<Unit, FlowyError>> moveRow({
+    required RowPB fromRow,
+    required String groupId,
+    RowPB? toRow,
+  }) {
     return _databaseViewBackendSvc.moveRow(
       fromRowId: fromRow.id,
       toGroupId: groupId,

+ 4 - 6
frontend/appflowy_flutter/lib/plugins/database_view/application/database_view_service.dart

@@ -46,15 +46,13 @@ class DatabaseViewBackendService {
 
   Future<Either<Unit, FlowyError>> moveRow({
     required String fromRowId,
-    required String? toGroupId,
-    required String? toRowId,
+    required String toGroupId,
+    String? toRowId,
   }) {
     var payload = MoveGroupRowPayloadPB.create()
       ..viewId = viewId
-      ..fromRowId = fromRowId;
-    if (toGroupId != null) {
-      payload.toGroupId = toGroupId;
-    }
+      ..fromRowId = fromRowId
+      ..toGroupId = toGroupId;
 
     if (toRowId != null) {
       payload.toRowId = toRowId;

+ 2 - 2
frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart

@@ -54,7 +54,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
         final toRow = groupControllers[groupId]?.rowAtIndex(toIndex);
         if (fromRow != null) {
           _databaseController.moveRow(
-            fromRow,
+            fromRow: fromRow,
             toRow: toRow,
             groupId: groupId,
           );
@@ -70,7 +70,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
         final toRow = groupControllers[toGroupId]?.rowAtIndex(toIndex);
         if (fromRow != null) {
           _databaseController.moveRow(
-            fromRow,
+            fromRow: fromRow,
             toRow: toRow,
             groupId: toGroupId,
           );

+ 1 - 0
frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/toolbar/board_toolbar.dart

@@ -65,6 +65,7 @@ class _SettingButtonState extends State<_SettingButton> {
     return AppFlowyPopover(
       controller: popoverController,
       direction: PopoverDirection.leftWithTopAligned,
+      offset: const Offset(-8, 0),
       triggerActions: PopoverTriggerFlags.none,
       constraints: BoxConstraints.loose(const Size(260, 400)),
       margin: EdgeInsets.zero,

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

@@ -87,7 +87,7 @@ class _SwitchFieldButton extends StatelessWidget {
       asBarrier: true,
       triggerActions: PopoverTriggerFlags.click,
       mutex: popoverMutex,
-      offset: const Offset(20, 0),
+      offset: const Offset(8, 0),
       popupBuilder: (popOverContext) {
         return FieldTypeList(onSelectField: (newFieldType) {
           context

+ 2 - 2
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/date.dart

@@ -80,7 +80,7 @@ class DateTypeOptionWidget extends TypeOptionWidget {
       mutex: popoverMutex,
       asBarrier: true,
       triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
-      offset: const Offset(20, 0),
+      offset: const Offset(8, 0),
       constraints: BoxConstraints.loose(const Size(460, 440)),
       popupBuilder: (popoverContext) {
         return DateFormatList(
@@ -107,7 +107,7 @@ class DateTypeOptionWidget extends TypeOptionWidget {
       mutex: popoverMutex,
       asBarrier: true,
       triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
-      offset: const Offset(20, 0),
+      offset: const Offset(8, 0),
       constraints: BoxConstraints.loose(const Size(460, 440)),
       popupBuilder: (BuildContext popoverContext) {
         return TimeFormatList(

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/number.dart

@@ -77,7 +77,7 @@ class NumberTypeOptionWidget extends TypeOptionWidget {
               mutex: popoverMutex,
               triggerActions:
                   PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
-              offset: const Offset(20, 0),
+              offset: const Offset(8, 0),
               constraints: BoxConstraints.loose(const Size(460, 440)),
               margin: EdgeInsets.zero,
               child: Padding(

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/select_option.dart

@@ -203,7 +203,7 @@ class _OptionCellState extends State<_OptionCell> {
     return AppFlowyPopover(
       controller: _popoverController,
       mutex: widget.popoverMutex,
-      offset: const Offset(20, 0),
+      offset: const Offset(8, 0),
       margin: EdgeInsets.zero,
       asBarrier: true,
       constraints: BoxConstraints.loose(const Size(460, 460)),

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/sort_editor.dart

@@ -176,7 +176,7 @@ class _AddSortButtonState extends State<_AddSortButton> {
       mutex: widget.popoverMutex,
       direction: PopoverDirection.bottomWithLeftAligned,
       constraints: BoxConstraints.loose(const Size(200, 300)),
-      offset: const Offset(0, 10),
+      offset: const Offset(0, 8),
       triggerActions: PopoverTriggerFlags.none,
       asBarrier: true,
       child: SizedBox(

+ 18 - 15
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/filter_button.dart

@@ -30,20 +30,23 @@ class _FilterButtonState extends State<FilterButton> {
 
         return _wrapPopover(
           context,
-          FlowyTextButton(
-            LocaleKeys.grid_settings_filter.tr(),
-            fontColor: textColor,
-            fillColor: Colors.transparent,
-            hoverColor: AFThemeExtension.of(context).lightGreyHover,
-            padding: GridSize.typeOptionContentInsets,
-            onPressed: () {
-              final bloc = context.read<GridFilterMenuBloc>();
-              if (bloc.state.filters.isEmpty) {
-                _popoverController.show();
-              } else {
-                bloc.add(const GridFilterMenuEvent.toggleMenu());
-              }
-            },
+          SizedBox(
+            height: 26,
+            child: FlowyTextButton(
+              LocaleKeys.grid_settings_filter.tr(),
+              fontColor: textColor,
+              fillColor: Colors.transparent,
+              hoverColor: AFThemeExtension.of(context).lightGreyHover,
+              padding: GridSize.typeOptionContentInsets,
+              onPressed: () {
+                final bloc = context.read<GridFilterMenuBloc>();
+                if (bloc.state.filters.isEmpty) {
+                  _popoverController.show();
+                } else {
+                  bloc.add(const GridFilterMenuEvent.toggleMenu());
+                }
+              },
+            ),
           ),
         );
       },
@@ -55,7 +58,7 @@ class _FilterButtonState extends State<FilterButton> {
       controller: _popoverController,
       direction: PopoverDirection.bottomWithLeftAligned,
       constraints: BoxConstraints.loose(const Size(200, 300)),
-      offset: const Offset(0, 10),
+      offset: const Offset(0, 8),
       triggerActions: PopoverTriggerFlags.none,
       child: child,
       popupBuilder: (BuildContext context) {

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

@@ -109,7 +109,7 @@ class _GridPropertyCellState extends State<_GridPropertyCell> {
     return AppFlowyPopover(
       mutex: widget.popoverMutex,
       controller: _popoverController,
-      offset: const Offset(20, 0),
+      offset: const Offset(8, 0),
       direction: PopoverDirection.leftWithTopAligned,
       constraints: BoxConstraints.loose(const Size(240, 400)),
       triggerActions: PopoverTriggerFlags.none,

+ 19 - 16
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/setting_button.dart

@@ -41,23 +41,26 @@ class _SettingButtonState extends State<SettingButton> {
         );
       },
       builder: (context, settingContext) {
-        return AppFlowyPopover(
-          controller: _popoverController,
-          constraints: BoxConstraints.loose(const Size(260, 400)),
-          direction: PopoverDirection.bottomWithLeftAligned,
-          offset: const Offset(0, 10),
-          margin: EdgeInsets.zero,
-          triggerActions: PopoverTriggerFlags.none,
-          child: FlowyTextButton(
-            LocaleKeys.settings_title.tr(),
-            fillColor: Colors.transparent,
-            hoverColor: AFThemeExtension.of(context).lightGreyHover,
-            padding: GridSize.typeOptionContentInsets,
-            onPressed: () => _popoverController.show(),
+        return SizedBox(
+          height: 26,
+          child: AppFlowyPopover(
+            controller: _popoverController,
+            constraints: BoxConstraints.loose(const Size(260, 400)),
+            direction: PopoverDirection.bottomWithLeftAligned,
+            offset: const Offset(0, 8),
+            margin: EdgeInsets.zero,
+            triggerActions: PopoverTriggerFlags.none,
+            child: FlowyTextButton(
+              LocaleKeys.settings_title.tr(),
+              fillColor: Colors.transparent,
+              hoverColor: AFThemeExtension.of(context).lightGreyHover,
+              padding: GridSize.typeOptionContentInsets,
+              onPressed: () => _popoverController.show(),
+            ),
+            popupBuilder: (BuildContext context) {
+              return _GridSettingListPopover(settingContext: settingContext);
+            },
           ),
-          popupBuilder: (BuildContext context) {
-            return _GridSettingListPopover(settingContext: settingContext);
-          },
         );
       },
     );

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/sort_button.dart

@@ -58,7 +58,7 @@ class _SortButtonState extends State<SortButton> {
       controller: _popoverController,
       direction: PopoverDirection.bottomWithLeftAligned,
       constraints: BoxConstraints.loose(const Size(200, 300)),
-      offset: const Offset(0, 10),
+      offset: const Offset(0, 8),
       margin: const EdgeInsets.all(6),
       triggerActions: PopoverTriggerFlags.none,
       child: child,

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor.dart

@@ -145,7 +145,7 @@ class _ChecklistOptionCellState extends State<_ChecklistOptionCell> {
   Widget _wrapPopover(Widget child) {
     return AppFlowyPopover(
       controller: _popoverController,
-      offset: const Offset(20, 0),
+      offset: const Offset(8, 0),
       asBarrier: true,
       constraints: BoxConstraints.loose(const Size(200, 300)),
       mutex: widget.popoverMutex,

+ 3 - 3
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart

@@ -376,7 +376,7 @@ class _DateTypeOptionButton extends StatelessWidget {
         return AppFlowyPopover(
           mutex: popoverMutex,
           triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
-          offset: const Offset(20, 0),
+          offset: const Offset(8, 0),
           margin: EdgeInsets.zero,
           constraints: BoxConstraints.loose(const Size(140, 100)),
           child: Padding(
@@ -431,7 +431,7 @@ class _CalDateTimeSettingState extends State<_CalDateTimeSetting> {
       AppFlowyPopover(
         mutex: timeSettingPopoverMutex,
         triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
-        offset: const Offset(20, 0),
+        offset: const Offset(8, 0),
         popupBuilder: (BuildContext context) {
           return DateFormatList(
             selectedFormat: widget.dateTypeOptionPB.dateFormat,
@@ -449,7 +449,7 @@ class _CalDateTimeSettingState extends State<_CalDateTimeSetting> {
       AppFlowyPopover(
         mutex: timeSettingPopoverMutex,
         triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
-        offset: const Offset(20, 0),
+        offset: const Offset(8, 0),
         popupBuilder: (BuildContext context) {
           return TimeFormatList(
               selectedFormat: widget.dateTypeOptionPB.timeFormat,

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor.dart

@@ -285,7 +285,7 @@ class _SelectOptionCellState extends State<_SelectOptionCell> {
     );
     return AppFlowyPopover(
       controller: _popoverController,
-      offset: const Offset(20, 0),
+      offset: const Offset(8, 0),
       margin: EdgeInsets.zero,
       asBarrier: true,
       constraints: BoxConstraints.loose(const Size(200, 460)),

+ 2 - 2
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/url_cell/url_cell.dart

@@ -127,7 +127,7 @@ class _GridURLCellState extends GridCellState<GridURLCell> {
             constraints: BoxConstraints.loose(const Size(300, 160)),
             direction: PopoverDirection.bottomWithLeftAligned,
             triggerActions: PopoverTriggerFlags.none,
-            offset: const Offset(0, 20),
+            offset: const Offset(0, 8),
             child: SizedBox.expand(
               child: GestureDetector(
                 child: Align(alignment: Alignment.centerLeft, child: richText),
@@ -210,7 +210,7 @@ class _EditURLAccessoryState extends State<_EditURLAccessory>
       constraints: BoxConstraints.loose(const Size(300, 160)),
       controller: _popoverController,
       direction: PopoverDirection.bottomWithLeftAligned,
-      offset: const Offset(0, 20),
+      offset: const Offset(0, 8),
       child: svgWidget(
         "editor/edit",
         color: Theme.of(context).colorScheme.onSurface,

+ 52 - 44
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/cover_image_picker_bloc.dart

@@ -12,13 +12,15 @@ import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:dartz/dartz.dart';
 import 'package:http/http.dart' as http;
 import 'package:shared_preferences/shared_preferences.dart';
-import 'package:path/path.dart' as path;
+import 'package:path/path.dart' as p;
 import 'change_cover_popover.dart';
 
 part 'cover_image_picker_bloc.freezed.dart';
 
 class CoverImagePickerBloc
     extends Bloc<CoverImagePickerEvent, CoverImagePickerState> {
+  static const allowedExtensions = ['jpg', 'png', 'jpeg'];
+
   CoverImagePickerBloc() : super(const CoverImagePickerState.initial()) {
     on<CoverImagePickerEvent>(
       (event, emit) async {
@@ -28,7 +30,7 @@ class CoverImagePickerBloc
           },
           urlSubmit: (UrlSubmit urlSubmit) async {
             emit(const CoverImagePickerState.loading());
-            final validateImage = await _validateUrl(urlSubmit.path);
+            final validateImage = await _validateURL(urlSubmit.path);
             if (validateImage) {
               emit(CoverImagePickerState.networkImage(left(urlSubmit.path)));
             } else {
@@ -86,28 +88,22 @@ class CoverImagePickerBloc
     if (state is FileImagePicked) {
       try {
         final path = state.path;
-        final newPath = '$directory/${path.split("\\").last}';
+        final newPath = p.join(directory, p.split(path).last);
         final newFile = await File(path).copy(newPath);
         imagePaths.add(newFile.path);
-        await prefs.setStringList(kLocalImagesKey, imagePaths);
-        return imagePaths;
       } catch (e) {
         return null;
       }
     } else if (state is NetworkImagePicked) {
       try {
-        String? url = state.successOrFail.fold((path) => path, (r) => null);
+        final url = state.successOrFail.fold((path) => path, (r) => null);
         if (url != null) {
           final response = await http.get(Uri.parse(url));
-          final newPath =
-              "$directory/IMG_$_timeStampString.${_getExtention(url)}";
-
+          final newPath = p.join(directory, _networkImageName(url));
           final imageFile = File(newPath);
           await imageFile.create();
           await imageFile.writeAsBytes(response.bodyBytes);
           imagePaths.add(imageFile.absolute.path);
-          await prefs.setStringList(kLocalImagesKey, imagePaths);
-          return imagePaths;
         } else {
           return null;
         }
@@ -115,59 +111,71 @@ class CoverImagePickerBloc
         return null;
       }
     }
+    await prefs.setStringList(kLocalImagesKey, imagePaths);
+    return imagePaths;
   }
 
-  _pickImages() async {
-    FilePickerResult? result = await getIt<FilePickerService>().pickFiles(
+  Future<String?> _pickImages() async {
+    final result = await getIt<FilePickerService>().pickFiles(
       dialogTitle: LocaleKeys.document_plugins_cover_addLocalImage.tr(),
       allowMultiple: false,
       type: fp.FileType.image,
-      allowedExtensions: ['jpg', 'png', 'jpeg'],
+      allowedExtensions: allowedExtensions,
     );
     if (result != null && result.files.isNotEmpty) {
-      final path = result.files.first.path;
-      if (path != null) {
-        return path;
-      } else {
-        return null;
-      }
+      return result.files.first.path;
     }
     return null;
   }
 
   Future<String> _coverPath() async {
     final directory = await getIt<SettingsLocationCubit>().fetchLocation();
-    return Directory(path.join(directory, 'covers'))
+    return Directory(p.join(directory, 'covers'))
         .create(recursive: true)
         .then((value) => value.path);
   }
 
-  String get _timeStampString =>
-      DateTime.now().millisecondsSinceEpoch.toString();
-
-  String? _getExtention(String path) => path.contains(".jpg")
-      ? "jpg"
-      : path.contains(".png")
-          ? "png"
-          : path.contains(".jpeg")
-              ? "jpeg"
-              : (path.contains("auto=format") && path.contains("unsplash"))
-                  ? "jpeg"
-                  : null;
+  String _networkImageName(String url) {
+    return 'IMG_${DateTime.now().millisecondsSinceEpoch.toString()}.${_getExtention(
+      url,
+      fromNetwork: true,
+    )}';
+  }
 
-  _validateUrl(String path) async {
-    if (_getExtention(path) != null) {
-      try {
-        final response = await http.get(Uri.parse(path));
-        if (response.statusCode == 200) {
-          return true;
-        } else {
-          return false;
-        }
-      } catch (e) {
-        return false;
+  String? _getExtention(
+    String path, {
+    bool fromNetwork = false,
+  }) {
+    String? ext;
+    if (!fromNetwork) {
+      final extension = p.extension(path);
+      if (extension.isEmpty) {
+        return null;
       }
+      ext = extension.substring(1);
     } else {
+      final uri = Uri.parse(path);
+      final paramters = uri.queryParameters;
+      final dl = paramters['dl'];
+      if (dl != null) {
+        ext = p.extension(dl).substring(1);
+      }
+    }
+    if (allowedExtensions.contains(ext)) {
+      return ext;
+    }
+    return null;
+  }
+
+  Future<bool> _validateURL(String path) async {
+    final extension = _getExtention(path, fromNetwork: true);
+    if (extension == null) {
+      return false;
+    }
+    try {
+      final response = await http.head(Uri.parse(path));
+      return response.statusCode == 200;
+    } catch (e) {
       return false;
     }
   }

+ 1 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_button.dart

@@ -82,6 +82,7 @@ class ShareActionList extends StatelessWidget {
     final docShareBloc = context.read<DocShareBloc>();
     return PopoverActionList<ShareActionWrapper>(
       direction: PopoverDirection.bottomWithCenterAligned,
+      offset: const Offset(0, 8),
       actions: ShareAction.values
           .map((action) => ShareActionWrapper(action))
           .toList(),

+ 7 - 4
frontend/appflowy_flutter/lib/user/application/user_listener.dart

@@ -83,11 +83,10 @@ class UserWorkspaceListener {
       PublishNotifier();
 
   FolderNotificationListener? _listener;
-  final UserProfilePB _userProfile;
 
   UserWorkspaceListener({
     required UserProfilePB userProfile,
-  }) : _userProfile = userProfile;
+  });
 
   void start({
     void Function(AuthNotifyValue)? onAuthChanged,
@@ -106,14 +105,18 @@ class UserWorkspaceListener {
       _settingChangedNotifier?.addPublishListener(onSettingUpdated);
     }
 
+    // The "current-workspace" is predefined in the backend. Do not try to
+    // modify it
     _listener = FolderNotificationListener(
-      objectId: _userProfile.token,
+      objectId: "current-workspace",
       handler: _handleObservableType,
     );
   }
 
   void _handleObservableType(
-      FolderNotification ty, Either<Uint8List, FlowyError> result) {
+    FolderNotification ty,
+    Either<Uint8List, FlowyError> result,
+  ) {
     switch (ty) {
       case FolderNotification.DidCreateWorkspace:
       case FolderNotification.DidDeleteWorkspace:

+ 1 - 0
frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/header/add_button.dart

@@ -48,6 +48,7 @@ class AddButton extends StatelessWidget {
     return PopoverActionList<PopoverAction>(
       direction: PopoverDirection.bottomWithLeftAligned,
       actions: actions,
+      offset: const Offset(0, 8),
       buildChild: (controller) {
         return FlowyIconButton(
           width: 22,

+ 1 - 0
frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart

@@ -42,6 +42,7 @@ class BubbleActionList extends StatelessWidget {
     return PopoverActionList<PopoverAction>(
       direction: PopoverDirection.topWithRightAligned,
       actions: actions,
+      offset: const Offset(0, -8),
       buildChild: (controller) {
         return FlowyTextButton(
           '?',

+ 3 - 0
frontend/appflowy_flutter/lib/workspace/presentation/widgets/pop_up_action.dart

@@ -13,6 +13,7 @@ class PopoverActionList<T extends PopoverAction> extends StatefulWidget {
   final Widget Function(PopoverController) buildChild;
   final VoidCallback? onClosed;
   final bool asBarrier;
+  final Offset offset;
 
   const PopoverActionList({
     required this.actions,
@@ -22,6 +23,7 @@ class PopoverActionList<T extends PopoverAction> extends StatefulWidget {
     this.onClosed,
     this.direction = PopoverDirection.rightWithTopAligned,
     this.asBarrier = false,
+    this.offset = Offset.zero,
     this.constraints = const BoxConstraints(
       minWidth: 120,
       maxWidth: 460,
@@ -54,6 +56,7 @@ class _PopoverActionListState<T extends PopoverAction>
       constraints: widget.constraints,
       direction: widget.direction,
       mutex: widget.mutex,
+      offset: widget.offset,
       triggerActions: PopoverTriggerFlags.none,
       onClose: widget.onClosed,
       popupBuilder: (BuildContext popoverContext) {

+ 31 - 0
frontend/appflowy_tauri/src/appflowy_app/components/tests/TestGroup.tsx

@@ -69,6 +69,37 @@ async function moveKanbanBoardRow() {
   // Create row in no status group
   const firstGroup = databaseController.groups.getValue()[1];
   const secondGroup = databaseController.groups.getValue()[2];
+  // subscribe the group changes
+  firstGroup.subscribe({
+    onRemoveRow: (groupId, deleteRowId) => {
+      console.log(groupId + 'did remove:' + deleteRowId);
+    },
+    onInsertRow: (groupId, rowPB) => {
+      console.log(groupId + 'did insert:' + rowPB.id);
+    },
+    onUpdateRow: (groupId, rowPB) => {
+      console.log(groupId + 'did update:' + rowPB.id);
+    },
+    onCreateRow: (groupId, rowPB) => {
+      console.log(groupId + 'did create:' + rowPB.id);
+    },
+  });
+
+  secondGroup.subscribe({
+    onRemoveRow: (groupId, deleteRowId) => {
+      console.log(groupId + 'did remove:' + deleteRowId);
+    },
+    onInsertRow: (groupId, rowPB) => {
+      console.log(groupId + 'did insert:' + rowPB.id);
+    },
+    onUpdateRow: (groupId, rowPB) => {
+      console.log(groupId + 'did update:' + rowPB.id);
+    },
+    onCreateRow: (groupId, rowPB) => {
+      console.log(groupId + 'did create:' + rowPB.id);
+    },
+  });
+
   const row = firstGroup.rowAtIndex(0).unwrap();
   await databaseController.moveRow(row.id, secondGroup.groupId);
 

+ 13 - 5
frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_bd_svc.ts

@@ -61,12 +61,19 @@ export class DatabaseBackendService {
     return DatabaseEventCreateRow(payload);
   };
 
-  /// Move a row to another group
-  moveRow = (rowId: string, groupId?: string) => {
-    const payload = MoveGroupRowPayloadPB.fromObject({ view_id: this.viewId, from_row_id: rowId });
-    if (groupId !== undefined) {
-      payload.to_group_id = groupId;
+  /// Move the row from one group to another group
+  /// [groupId] can be the moving row's group id or others.
+  /// [toRowId] is used to locate the moving row location.
+  moveGroupRow = (fromRowId: string, groupId: string, toRowId?: string) => {
+    const payload = MoveGroupRowPayloadPB.fromObject({
+      view_id: this.viewId,
+      from_row_id: fromRowId,
+      to_group_id: groupId,
+    });
+    if (toRowId !== undefined) {
+      payload.to_row_id = toRowId;
     }
+
     return DatabaseEventMoveGroupRow(payload);
   };
 
@@ -106,6 +113,7 @@ export class DatabaseBackendService {
   };
 
   /// Get all groups in database
+  /// It should only call once after the board open
   loadGroups = () => {
     const payload = DatabaseViewIdPB.fromObject({ value: this.viewId });
     return DatabaseEventGetGroups(payload);

+ 1 - 1
frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_controller.ts

@@ -76,7 +76,7 @@ export class DatabaseController {
   };
 
   moveRow = (rowId: string, groupId: string) => {
-    return this.backendService.moveRow(rowId, groupId);
+    return this.backendService.moveGroupRow(rowId, groupId);
   };
 
   exchangeRow = async (fromRowId: string, toRowId: string) => {

+ 2 - 1
frontend/rust-lib/flowy-database/src/services/group/controller_impls/select_option_controller/util.rs

@@ -115,6 +115,8 @@ pub fn move_group_row(
     }
 
     // Update the corresponding row's cell content.
+    // If the from_index is none which means the row is not belong to this group before and
+    // it is moved from other groups.
     if from_index.is_none() {
       let cell_rev = make_inserted_cell_rev(&group.id, field_rev);
       if let Some(cell_rev) = cell_rev {
@@ -126,7 +128,6 @@ pub fn move_group_row(
         row_changeset
           .cell_by_field_id
           .insert(field_rev.id.clone(), cell_rev);
-        changeset.updated_rows.push(RowPB::from(*row_rev));
       }
     }
   }

+ 17 - 15
frontend/rust-lib/flowy-folder/src/services/workspace/controller.rs

@@ -11,6 +11,7 @@ use crate::{
 };
 use flowy_sqlite::kv::KV;
 use folder_model::{AppRevision, WorkspaceRevision};
+use lib_dispatch::prelude::ToBytes;
 use std::sync::Arc;
 
 pub struct WorkspaceController {
@@ -41,7 +42,6 @@ impl WorkspaceController {
   ) -> Result<WorkspaceRevision, FlowyError> {
     let workspace = self.create_workspace_on_server(params.clone()).await?;
     let user_id = self.user.user_id()?;
-    let token = self.user.token()?;
     let workspaces = self
       .persistence
       .begin_transaction(|transaction| {
@@ -53,9 +53,7 @@ impl WorkspaceController {
       .map(|workspace_rev| workspace_rev.into())
       .collect();
     let repeated_workspace = RepeatedWorkspacePB { items: workspaces };
-    send_notification(&token, FolderNotification::DidCreateWorkspace)
-      .payload(repeated_workspace)
-      .send();
+    send_workspace_notification(FolderNotification::DidCreateWorkspace, repeated_workspace);
     set_current_workspace(&user_id, &workspace.id);
     Ok(workspace)
   }
@@ -76,9 +74,7 @@ impl WorkspaceController {
       })
       .await?;
 
-    send_notification(&workspace_id, FolderNotification::DidUpdateWorkspace)
-      .payload(workspace)
-      .send();
+    send_workspace_notification(FolderNotification::DidUpdateWorkspace, workspace);
     self.update_workspace_on_server(params)?;
 
     Ok(())
@@ -87,7 +83,6 @@ impl WorkspaceController {
   #[allow(dead_code)]
   pub(crate) async fn delete_workspace(&self, workspace_id: &str) -> Result<(), FlowyError> {
     let user_id = self.user.user_id()?;
-    let token = self.user.token()?;
     let repeated_workspace = self
       .persistence
       .begin_transaction(|transaction| {
@@ -95,9 +90,8 @@ impl WorkspaceController {
         self.read_workspaces(None, &user_id, &transaction)
       })
       .await?;
-    send_notification(&token, FolderNotification::DidDeleteWorkspace)
-      .payload(repeated_workspace)
-      .send();
+
+    send_workspace_notification(FolderNotification::DidDeleteWorkspace, repeated_workspace);
     self.delete_workspace_on_server(workspace_id)?;
     Ok(())
   }
@@ -224,7 +218,6 @@ pub async fn notify_workspace_setting_did_change(
   view_id: &str,
 ) -> FlowyResult<()> {
   let user_id = folder_manager.user.user_id()?;
-  let token = folder_manager.user.token()?;
   let workspace_id = get_current_workspace(&user_id)?;
 
   let workspace_setting = folder_manager
@@ -250,11 +243,20 @@ pub async fn notify_workspace_setting_did_change(
       Ok(setting)
     })
     .await?;
+  send_workspace_notification(
+    FolderNotification::DidUpdateWorkspaceSetting,
+    workspace_setting,
+  );
+  Ok(())
+}
 
-  send_notification(&token, FolderNotification::DidUpdateWorkspaceSetting)
-    .payload(workspace_setting)
+/// The [CURRENT_WORKSPACE] represents as the current workspace that opened by the
+/// user. Only one workspace can be opened at a time.
+const CURRENT_WORKSPACE: &str = "current-workspace";
+fn send_workspace_notification<T: ToBytes>(ty: FolderNotification, payload: T) {
+  send_notification(CURRENT_WORKSPACE, ty)
+    .payload(payload)
     .send();
-  Ok(())
 }
 
 const CURRENT_WORKSPACE_ID: &str = "current_workspace_id";

+ 34 - 17
frontend/scripts/install_dev_env/install_linux.sh

@@ -6,39 +6,56 @@ RED="\e[31m"
 ENDCOLOR="\e[0m"
 
 printMessage() {
-   printf "${YELLOW}AppFlowy : $1${ENDCOLOR}\n"
+    printf "${YELLOW}AppFlowy : $1${ENDCOLOR}\n"
 }
 
 printSuccess() {
-   printf "${GREEN}AppFlowy : $1${ENDCOLOR}\n"
+    printf "${GREEN}AppFlowy : $1${ENDCOLOR}\n"
 }
 
 printError() {
-   printf "${RED}AppFlowy : $1${ENDCOLOR}\n"
+    printf "${RED}AppFlowy : $1${ENDCOLOR}\n"
 }
 
-
 # Note: This script does not install applications which are installed by the package manager. There are too many package managers out there.
 
-# Install Rust 
+# Install Rust
 printMessage "The Rust programming language is required to compile AppFlowy."
 printMessage "We can install it now if you don't already have it on your system."
 
 read -p "$(printSuccess "Do you want to install Rust? [y/N]") " installrust
 
 if [ ${installrust^^} == "Y" ]; then
-   printMessage "Installing Rust."
-   curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
-   source $HOME/.cargo/env
-   rustup toolchain install stable
-   rustup default stable
+    printMessage "Installing Rust."
+    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
+    source $HOME/.cargo/env
+    rustup toolchain install stable
+    rustup default stable
 else
-   printMessage "Skipping Rust installation."
+    printMessage "Skipping Rust installation."
 fi
 
-# Enable the flutter stable channel
 printMessage "Setting up Flutter"
-flutter channel stable
+# Get the current Flutter version
+FLUTTER_VERSION=$(flutter --version | grep -oP 'Flutter \K\S+')
+# Check if the current version is 3.3.10
+if [ "$FLUTTER_VERSION" = "3.3.10" ]; then
+    echo "Flutter version is already 3.3.10"
+else
+    # Get the path to the Flutter SDK
+    FLUTTER_PATH=$(which flutter)
+    FLUTTER_PATH=${FLUTTER_PATH%/bin/flutter}
+
+    current_dir=$(pwd)
+
+    cd $FLUTTER_PATH
+    # Use git to checkout version 3.3.10 of Flutter
+    git checkout 3.3.10
+    # Get back to current working directory
+    cd "$current_dir"
+
+    echo "Switched to Flutter version 3.3.10"
+fi
 
 # Enable linux desktop
 flutter config --enable-linux-desktop
@@ -47,9 +64,9 @@ flutter config --enable-linux-desktop
 flutter doctor
 
 printMessage "Installing keybinder-3.0"
-if command apt-get &> /dev/null; then
+if command apt-get &>/dev/null; then
     sudo apt-get install keybinder-3.0-dev
-elif command dnf &> /dev/null; then
+elif command dnf &>/dev/null; then
     sudo dnf install keybinder3-devel
 else
     echo 'Your system is not supported, please install keybinder3 manually.'
@@ -59,11 +76,11 @@ fi
 printMessage "Setting up githooks."
 git config core.hooksPath .githooks
 
-# Install go-gitlint 
+# Install go-gitlint
 printMessage "Installing go-gitlint."
 GOLINT_FILENAME="go-gitlint_1.1.0_linux_x86_64.tar.gz"
 wget https://github.com/llorllale/go-gitlint/releases/download/1.1.0/${GOLINT_FILENAME}
-tar -zxv --directory .githooks/. -f ${GOLINT_FILENAME} gitlint 
+tar -zxv --directory .githooks/. -f ${GOLINT_FILENAME} gitlint
 rm ${GOLINT_FILENAME}
 
 # Change to the frontend directory

+ 26 - 8
frontend/scripts/install_dev_env/install_macos.sh

@@ -17,8 +17,7 @@ printError() {
    printf "${RED}AppFlowy : $1${ENDCOLOR}\n"
 }
 
-
-# Install Rust 
+# Install Rust
 printMessage "The Rust programming language is required to compile AppFlowy."
 printMessage "We can install it now if you don't already have it on your system."
 
@@ -28,7 +27,7 @@ if [[ "${installrust:-N}" == [Yy] ]]; then
    printMessage "Installing Rust."
    brew install rustup-init
    rustup-init -y --default-toolchain=stable
-  
+
    source "$HOME/.cargo/env"
 else
    printMessage "Skipping Rust installation."
@@ -36,11 +35,30 @@ fi
 
 # Install sqllite
 printMessage "Installing sqlLite3."
-brew install sqlite3 
+brew install sqlite3
 
-# Enable the flutter stable channel
 printMessage "Setting up Flutter"
-flutter channel stable
+
+# Get the current Flutter version
+FLUTTER_VERSION=$(flutter --version | grep -oE 'Flutter [^ ]+' | grep -oE '[^ ]+$')
+# Check if the current version is 3.3.10
+if [ "$FLUTTER_VERSION" = "3.3.10" ]; then
+   echo "Flutter version is already 3.3.10"
+else
+   # Get the path to the Flutter SDK
+   FLUTTER_PATH=$(which flutter)
+   FLUTTER_PATH=${FLUTTER_PATH%/bin/flutter}
+
+   current_dir=$(pwd)
+
+   cd $FLUTTER_PATH
+   # Use git to checkout version 3.3.10 of Flutter
+   git checkout 3.3.10
+   # Get back to current working directory
+   cd "$current_dir"
+
+   echo "Switched to Flutter version 3.3.10"
+fi
 
 # Enable linux desktop
 flutter config --enable-macos-desktop
@@ -52,11 +70,11 @@ flutter doctor
 printMessage "Setting up githooks."
 git config core.hooksPath .githooks
 
-# Install go-gitlint 
+# Install go-gitlint
 printMessage "Installing go-gitlint."
 GOLINT_FILENAME="go-gitlint_1.1.0_osx_x86_64.tar.gz"
 curl -L https://github.com/llorllale/go-gitlint/releases/download/1.1.0/${GOLINT_FILENAME} --output ${GOLINT_FILENAME}
-tar -zxv --directory .githooks/. -f ${GOLINT_FILENAME} gitlint 
+tar -zxv --directory .githooks/. -f ${GOLINT_FILENAME} gitlint
 rm ${GOLINT_FILENAME}
 
 # Change to the frontend directory

+ 22 - 5
frontend/scripts/install_dev_env/install_windows.sh

@@ -17,7 +17,6 @@ printError() {
    printf "${RED}AppFlowy : $1${ENDCOLOR}\n"
 }
 
-
 # Note: This script does not install applications which are installed by the package manager. There are too many package managers out there.
 
 # Install Rust
@@ -46,9 +45,27 @@ else
    printSuccess "Rust has been detected on your system, so Rust installation has been skipped"
 fi
 
-# Enable the flutter stable channel
 printMessage "Setting up Flutter"
-flutter channel stable
+# Get the current Flutter version
+FLUTTER_VERSION=$(flutter --version | grep -oP 'Flutter \K\S+')
+# Check if the current version is 3.3.10
+if [ "$FLUTTER_VERSION" = "3.3.10" ]; then
+   echo "Flutter version is already 3.3.10"
+else
+   # Get the path to the Flutter SDK
+   FLUTTER_PATH=$(which flutter)
+   FLUTTER_PATH=${FLUTTER_PATH%/bin/flutter}
+
+   current_dir=$(pwd)
+
+   cd $FLUTTER_PATH
+   # Use git to checkout version 3.3.10 of Flutter
+   git checkout 3.3.10
+   # Get back to current working directory
+   cd "$current_dir"
+
+   echo "Switched to Flutter version 3.3.10"
+fi
 
 # Add pub cache and cargo to PATH
 powershell '[Environment]::SetEnvironmentVariable("PATH", $Env:PATH + ";" + $Env:LOCALAPPDATA + "\Pub\Cache\Bin", [EnvironmentVariableTarget]::User)'
@@ -64,14 +81,14 @@ flutter doctor
 printMessage "Setting up githooks."
 git config core.hooksPath .githooks
 
-# Install go-gitlint 
+# Install go-gitlint
 printMessage "Installing go-gitlint."
 GOLINT_FILENAME="go-gitlint_1.1.0_windows_x86_64.tar.gz"
 if curl --proto '=https' --tlsv1.2 -sSfL https://github.com/llorllale/go-gitlint/releases/download/1.1.0/${GOLINT_FILENAME} -o ${GOLINT_FILENAME}; then
    tar -zxv --directory .githooks/. -f ${GOLINT_FILENAME} gitlint.exe
    rm ${GOLINT_FILENAME}
 else
- printError "Failed to install go-gitlint"
+   printError "Failed to install go-gitlint"
 fi
 
 # Change to the frontend directory