feat: support private section (#4882)

This commit is contained in:
Lucas.Xu 2024-03-21 11:02:03 +07:00 committed by GitHub
parent 9201cd6347
commit ef9891abfe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
75 changed files with 1758 additions and 776 deletions

View File

@ -115,9 +115,12 @@
},
{
"name": "AF-desktop: Debug Rust",
"request": "attach",
"type": "lldb",
"request": "attach",
"pid": "${command:pickMyProcess}"
// To launch the application directly, use the following configuration:
// "request": "launch",
// "program": "[YOUR_APPLICATION_PATH]",
},
{
// https://tauri.app/v1/guides/debugging/vs-code

View File

@ -1,6 +1,7 @@
// ignore_for_file: unused_import
import 'dart:io';
import 'dart:ui';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
@ -14,8 +15,9 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as p;
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import '../shared/dir.dart';
import '../shared/mock/mock_file_picker.dart';
import '../shared/util.dart';

View File

@ -28,15 +28,16 @@ void main() {
final email = '${uuid()}@appflowy.io';
testWidgets('change name and icon', (tester) async {
// only run the test when the feature flag is on
if (!FeatureFlag.collaborativeWorkspace.isOn) {
return;
}
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
email: email, // use the same email to check the next test
);
// turn on the collaborative workspace feature flag before testing,
// if the feature is released to the public, this step can be removed
await FeatureFlag.collaborativeWorkspace.turnOn();
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
@ -57,15 +58,16 @@ void main() {
});
testWidgets('verify the result again after relaunching', (tester) async {
// only run the test when the feature flag is on
if (!FeatureFlag.collaborativeWorkspace.isOn) {
return;
}
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
email: email, // use the same email to check the next test
);
// turn on the collaborative workspace feature flag before testing,
// if the feature is released to the public, this step can be removed
await FeatureFlag.collaborativeWorkspace.turnOn();
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();

View File

@ -35,14 +35,14 @@ void main() {
final email = '${uuid()}@appflowy.io';
group('collaborative workspace', () {
// combine the create and delete workspace test to reduce the time
testWidgets('create a new workspace, open it and then delete it',
(tester) async {
// only run the test when the feature flag is on
if (!FeatureFlag.collaborativeWorkspace.isOn) {
return;
}
// combine the create and delete workspace test to reduce the time
testWidgets('create a new workspace, open it and then delete it',
(tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
email: email,

View File

@ -1,6 +1,6 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_folder.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_test/flutter_test.dart';
@ -13,10 +13,10 @@ void main() {
group('sidebar expand test', () {
bool isExpanded({required FolderCategoryType type}) {
if (type == FolderCategoryType.personal) {
if (type == FolderCategoryType.private) {
return find
.descendant(
of: find.byType(PersonalFolder),
of: find.byType(PrivateSectionFolder),
matching: find.byType(ViewItem),
)
.evaluate()
@ -30,19 +30,19 @@ void main() {
await tester.tapGoButton();
// first time is expanded
expect(isExpanded(type: FolderCategoryType.personal), true);
expect(isExpanded(type: FolderCategoryType.private), true);
// collapse the personal folder
await tester.tapButton(
find.byTooltip(LocaleKeys.sideBar_clickToHidePersonal.tr()),
find.byTooltip(LocaleKeys.sideBar_clickToHidePrivate.tr()),
);
expect(isExpanded(type: FolderCategoryType.personal), false);
expect(isExpanded(type: FolderCategoryType.private), false);
// expand the personal folder
await tester.tapButton(
find.byTooltip(LocaleKeys.sideBar_clickToHidePersonal.tr()),
find.byTooltip(LocaleKeys.sideBar_clickToHidePrivate.tr()),
);
expect(isExpanded(type: FolderCategoryType.personal), true);
expect(isExpanded(type: FolderCategoryType.private), true);
});
});
}

View File

@ -1,5 +1,5 @@
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/favorite_folder.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/_favorite_folder.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';

View File

@ -1,6 +1,5 @@
import 'package:integration_test/integration_test.dart';
import 'sidebar_expand_test.dart' as sidebar_expanded_test;
import 'sidebar_favorites_test.dart' as sidebar_favorite_test;
import 'sidebar_icon_test.dart' as sidebar_icon_test;
import 'sidebar_test.dart' as sidebar_test;
@ -10,7 +9,7 @@ void startTesting() {
// Sidebar integration tests
sidebar_test.main();
sidebar_expanded_test.main();
// sidebar_expanded_test.main();
sidebar_favorite_test.main();
sidebar_icon_test.main();
}

View File

@ -1,14 +1,18 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/presentation/home/personal_folder/mobile_home_personal_folder.dart';
import 'package:appflowy/mobile/presentation/home/section_folder/mobile_home_section_folder.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart';
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.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_slidable/flutter_slidable.dart';
// Contains Public And Private Sections
class MobileFolders extends StatelessWidget {
const MobileFolders({
super.key,
@ -26,9 +30,9 @@ class MobileFolders extends StatelessWidget {
return MultiBlocProvider(
providers: [
BlocProvider(
create: (_) => SidebarRootViewsBloc()
create: (_) => SidebarSectionsBloc()
..add(
SidebarRootViewsEvent.initial(
SidebarSectionsEvent.initial(
user,
workspaceSetting.workspaceId,
),
@ -38,31 +42,46 @@ class MobileFolders extends StatelessWidget {
create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()),
),
],
child: MultiBlocListener(
listeners: [
BlocListener<SidebarRootViewsBloc, SidebarRootViewState>(
child: BlocConsumer<SidebarSectionsBloc, SidebarSectionsState>(
listenWhen: (p, c) =>
p.lastCreatedRootView?.id != c.lastCreatedRootView?.id,
listener: (context, state) =>
context.pushView(state.lastCreatedRootView!),
),
],
child: Builder(
builder: (context) {
final menuState = context.watch<SidebarRootViewsBloc>().state;
listener: (context, state) {
final lastCreatedRootView = state.lastCreatedRootView;
if (lastCreatedRootView != null) {
context.pushView(lastCreatedRootView);
}
},
builder: (context, state) {
final isCollaborativeWorkspace =
user.authenticator != AuthenticatorPB.Local &&
FeatureFlag.collaborativeWorkspace.isOn;
return SlidableAutoCloseBehavior(
child: Column(
children: [
MobilePersonalFolder(
views: menuState.views,
...isCollaborativeWorkspace
? [
MobileSectionFolder(
title: LocaleKeys.sideBar_public.tr(),
views: state.section.publicViews,
),
const VSpace(8.0),
MobileSectionFolder(
title: LocaleKeys.sideBar_private.tr(),
views: state.section.privateViews,
),
]
: [
MobileSectionFolder(
title: LocaleKeys.sideBar_personal.tr(),
views: state.section.publicViews,
),
],
const VSpace(8.0),
],
),
);
},
),
),
);
}
}

View File

@ -8,6 +8,7 @@ import 'package:appflowy/mobile/presentation/home/mobile_home_page_header.dart';
import 'package:appflowy/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
@ -15,6 +16,7 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
@ -82,6 +84,16 @@ class MobileHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => UserWorkspaceBloc(userProfile: userProfile)
..add(
const UserWorkspaceEvent.initial(),
),
child: BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(
buildWhen: (previous, current) =>
previous.currentWorkspace?.workspaceId !=
current.currentWorkspace?.workspaceId,
builder: (context, state) {
return Column(
children: [
// Header
@ -132,6 +144,9 @@ class MobileHomePage extends StatelessWidget {
),
],
);
},
),
);
}
}

View File

@ -3,10 +3,13 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/home/mobile_home_setting_page.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart';
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/user/settings_user_bloc.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@ -14,7 +17,10 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class MobileHomePageHeader extends StatelessWidget {
const MobileHomePageHeader({super.key, required this.userProfile});
const MobileHomePageHeader({
super.key,
required this.userProfile,
});
final UserProfilePB userProfile;
@ -25,29 +31,18 @@ class MobileHomePageHeader extends StatelessWidget {
..add(const SettingsUserEvent.initial()),
child: BlocBuilder<SettingsUserViewBloc, SettingsUserState>(
builder: (context, state) {
final userIcon = state.userProfile.iconUrl;
final isCollaborativeWorkspace =
userProfile.authenticator != AuthenticatorPB.Local &&
FeatureFlag.collaborativeWorkspace.isOn;
return ConstrainedBox(
constraints: const BoxConstraints(minHeight: 52),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_UserIcon(userIcon: userIcon),
const HSpace(12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const FlowyText.medium('AppFlowy', fontSize: 18),
const VSpace(4),
FlowyText.regular(
userProfile.email.isNotEmpty
? state.userProfile.email
: state.userProfile.name,
fontSize: 12,
color: Theme.of(context).colorScheme.onSurface,
overflow: TextOverflow.ellipsis,
),
],
),
child: isCollaborativeWorkspace
? _MobileWorkspace(userProfile: userProfile)
: _MobileUser(userProfile: userProfile),
),
IconButton(
onPressed: () =>
@ -63,6 +58,83 @@ class MobileHomePageHeader extends StatelessWidget {
}
}
class _MobileUser extends StatelessWidget {
const _MobileUser({
required this.userProfile,
});
final UserProfilePB userProfile;
@override
Widget build(BuildContext context) {
final userIcon = userProfile.iconUrl;
return Row(
children: [
_UserIcon(userIcon: userIcon),
const HSpace(12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const FlowyText.medium('AppFlowy', fontSize: 18),
const VSpace(4),
FlowyText.regular(
userProfile.email.isNotEmpty
? userProfile.email
: userProfile.name,
fontSize: 12,
color: Theme.of(context).colorScheme.onSurface,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
);
}
}
class _MobileWorkspace extends StatelessWidget {
const _MobileWorkspace({
required this.userProfile,
});
final UserProfilePB userProfile;
@override
Widget build(BuildContext context) {
return BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(
builder: (context, state) {
final currentWorkspace = state.currentWorkspace;
final workspaces = state.workspaces;
if (currentWorkspace == null || workspaces.isEmpty) {
return const SizedBox.shrink();
}
return Row(
children: [
const HSpace(2.0),
SizedBox.square(
dimension: 34.0,
child: WorkspaceIcon(
workspace: currentWorkspace,
iconSize: 26,
enableEdit: false,
),
),
const HSpace(8),
Expanded(
child: FlowyText.medium(
currentWorkspace.name,
overflow: TextOverflow.ellipsis,
),
),
],
);
},
);
}
}
class _UserIcon extends StatelessWidget {
const _UserIcon({
required this.userIcon,

View File

@ -1,6 +1,6 @@
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart';
import 'package:appflowy/mobile/presentation/home/personal_folder/mobile_home_personal_folder_header.dart';
import 'package:appflowy/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart';
import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
@ -9,18 +9,20 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MobilePersonalFolder extends StatelessWidget {
const MobilePersonalFolder({
class MobileSectionFolder extends StatelessWidget {
const MobileSectionFolder({
super.key,
required this.title,
required this.views,
});
final String title;
final List<ViewPB> views;
@override
Widget build(BuildContext context) {
return BlocProvider<FolderBloc>(
create: (context) => FolderBloc(type: FolderCategoryType.personal)
create: (context) => FolderBloc(type: FolderCategoryType.private)
..add(
const FolderEvent.initial(),
),
@ -28,7 +30,8 @@ class MobilePersonalFolder extends StatelessWidget {
builder: (context, state) {
return Column(
children: [
MobilePersonalFolderHeader(
MobileSectionFolderHeader(
title: title,
isExpanded: context.read<FolderBloc>().state.isExpanded,
onPressed: () => context
.read<FolderBloc>()
@ -45,9 +48,9 @@ class MobilePersonalFolder extends StatelessWidget {
...views.map(
(view) => MobileViewItem(
key: ValueKey(
'${FolderCategoryType.personal.name} ${view.id}',
'${FolderCategoryType.private.name} ${view.id}',
),
categoryType: FolderCategoryType.personal,
categoryType: FolderCategoryType.private,
isFirstChild: view.id == views.first.id,
view: view,
level: 0,

View File

@ -1,30 +1,32 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MobilePersonalFolderHeader extends StatefulWidget {
const MobilePersonalFolderHeader({
class MobileSectionFolderHeader extends StatefulWidget {
const MobileSectionFolderHeader({
super.key,
required this.title,
required this.onPressed,
required this.onAdded,
required this.isExpanded,
});
final String title;
final VoidCallback onPressed;
final VoidCallback onAdded;
final bool isExpanded;
@override
State<MobilePersonalFolderHeader> createState() =>
_MobilePersonalFolderHeaderState();
State<MobileSectionFolderHeader> createState() =>
_MobileSectionFolderHeaderState();
}
class _MobilePersonalFolderHeaderState
extends State<MobilePersonalFolderHeader> {
class _MobileSectionFolderHeaderState extends State<MobileSectionFolderHeader> {
double _turns = 0;
@override
@ -35,7 +37,7 @@ class _MobilePersonalFolderHeaderState
Expanded(
child: FlowyButton(
text: FlowyText.semibold(
LocaleKeys.sideBar_personal.tr(),
widget.title,
fontSize: 20.0,
),
margin: const EdgeInsets.symmetric(vertical: 8),
@ -71,6 +73,7 @@ class _MobilePersonalFolderHeaderState
SidebarRootViewsEvent.createRootView(
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
index: 0,
viewSection: ViewSectionPB.Private,
),
);
},

View File

@ -4,7 +4,7 @@ import 'package:appflowy/mobile/presentation/notifications/widgets/mobile_notifi
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/notification_filter/notification_filter_bloc.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart';
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart';
import 'package:appflowy/workspace/presentation/notifications/reminder_extension.dart';
import 'package:appflowy/workspace/presentation/notifications/widgets/inbox_action_bar.dart';
@ -80,15 +80,15 @@ class _NotificationScreenContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => SidebarRootViewsBloc()
create: (_) => SidebarSectionsBloc()
..add(
SidebarRootViewsEvent.initial(
SidebarSectionsEvent.initial(
userProfile,
workspaceSetting.workspaceId,
),
),
child: BlocBuilder<SidebarRootViewsBloc, SidebarRootViewState>(
builder: (context, menuState) =>
child: BlocBuilder<SidebarSectionsBloc, SidebarSectionsState>(
builder: (context, sectionState) =>
BlocBuilder<NotificationFilterBloc, NotificationFilterState>(
builder: (context, filterState) =>
BlocBuilder<ReminderBloc, ReminderState>(
@ -122,7 +122,7 @@ class _NotificationScreenContent extends StatelessWidget {
NotificationsView(
shownReminders: pastReminders,
reminderBloc: reminderBloc,
views: menuState.views,
views: sectionState.section.publicViews,
onAction: _onAction,
onDelete: _onDelete,
onReadChanged: _onReadChanged,
@ -134,7 +134,7 @@ class _NotificationScreenContent extends StatelessWidget {
NotificationsView(
shownReminders: upcomingReminders,
reminderBloc: reminderBloc,
views: menuState.views,
views: sectionState.section.publicViews,
isUpcoming: true,
onAction: _onAction,
),

View File

@ -406,6 +406,7 @@ class _SingleMobileInnerViewItemState extends State<SingleMobileInnerViewItem> {
ViewEvent.createView(
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
layout,
section: widget.categoryType.toViewSectionPB,
),
);
},

View File

@ -6,6 +6,7 @@ import 'package:appflowy/plugins/document/application/editor_transaction_adapter
import 'package:appflowy/plugins/trash/application/trash_service.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/util/json_print.dart';
import 'package:appflowy/workspace/application/doc/doc_listener.dart';
import 'package:appflowy/workspace/application/doc/sync_state_listener.dart';
import 'package:appflowy/workspace/application/view/view_listener.dart';
@ -81,30 +82,24 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
final editorState = await _fetchDocumentState();
_onViewChanged();
_onDocumentChanged();
await editorState.fold(
final newState = await editorState.fold(
(s) async {
final result = await getIt<AuthService>().getUser();
final userProfilePB = result.fold(
(s) => s,
(e) => null,
);
emit(
state.copyWith(
final userProfilePB =
await getIt<AuthService>().getUser().toNullable();
return state.copyWith(
error: null,
editorState: s,
isLoading: false,
userProfilePB: userProfilePB,
),
);
},
(f) async => emit(
state.copyWith(
(f) async => state.copyWith(
error: f,
editorState: null,
isLoading: false,
),
),
);
emit(newState);
},
moveToTrash: () async {
emit(state.copyWith(isDeleted: true));
@ -242,21 +237,20 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
}
void syncDocumentDataPB(DocEventPB docEvent) {
// prettyPrintJson(docEvent.toProto3Json());
// todo: integrate the document change to the editor
// for (final event in docEvent.events) {
// for (final blockEvent in event.event) {
// switch (blockEvent.command) {
// case DeltaTypePB.Inserted:
// break;
// case DeltaTypePB.Updated:
// break;
// case DeltaTypePB.Removed:
// break;
// default:
// }
// }
// }
prettyPrintJson(docEvent.toProto3Json());
for (final event in docEvent.events) {
for (final blockEvent in event.event) {
switch (blockEvent.command) {
case DeltaTypePB.Inserted:
break;
case DeltaTypePB.Updated:
break;
case DeltaTypePB.Removed:
break;
default:
}
}
}
}
}

View File

@ -1,9 +1,9 @@
import 'dart:collection';
import 'dart:convert';
import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:collection/collection.dart';
typedef FeatureFlagMap = Map<FeatureFlag, bool>;
@ -19,16 +19,22 @@ enum FeatureFlag {
// used to control the visibility of the members settings
// if it's on, you can see the members settings in the settings page
membersSettings;
membersSettings,
// used for ignore the conflicted feature flag
unknown;
static Future<void> initialize() async {
final values = await getIt<KeyValueStorage>().getWithFormat<FeatureFlagMap>(
KVKeys.featureFlag,
(value) => Map.from(jsonDecode(value)).map(
(key, value) => MapEntry(
FeatureFlag.values.firstWhere((e) => e.name == key),
value as bool,
),
(key, value) {
final k = FeatureFlag.values.firstWhereOrNull(
(e) => e.name == key,
) ??
FeatureFlag.unknown;
return MapEntry(k, value as bool);
},
),
) ??
{};
@ -76,6 +82,8 @@ enum FeatureFlag {
return false;
case FeatureFlag.membersSettings:
return false;
case FeatureFlag.unknown:
return false;
}
}
@ -85,6 +93,8 @@ enum FeatureFlag {
return 'if it\'s on, you can see the workspace list and the workspace settings in the top-left corner of the app';
case FeatureFlag.membersSettings:
return 'if it\'s on, you can see the members settings in the settings page';
case FeatureFlag.unknown:
return '';
}
}

View File

@ -150,4 +150,44 @@ class UserBackendService {
..newIcon = icon;
return UserEventChangeWorkspaceIcon(request).send();
}
Future<FlowyResult<RepeatedWorkspaceMemberPB, FlowyError>>
getWorkspaceMembers(
String workspaceId,
) async {
final data = QueryWorkspacePB()..workspaceId = workspaceId;
return UserEventGetWorkspaceMember(data).send();
}
Future<FlowyResult<void, FlowyError>> addWorkspaceMember(
String workspaceId,
String email,
) async {
final data = AddWorkspaceMemberPB()
..workspaceId = workspaceId
..email = email;
return UserEventAddWorkspaceMember(data).send();
}
Future<FlowyResult<void, FlowyError>> removeWorkspaceMember(
String workspaceId,
String email,
) async {
final data = RemoveWorkspaceMemberPB()
..workspaceId = workspaceId
..email = email;
return UserEventRemoveWorkspaceMember(data).send();
}
Future<FlowyResult<void, FlowyError>> updateWorkspaceMember(
String workspaceId,
String email,
AFRolePB role,
) async {
final data = UpdateWorkspaceMemberPB()
..workspaceId = workspaceId
..email = email
..role = role;
return UserEventUpdateWorkspaceMember(data).send();
}
}

View File

@ -33,18 +33,19 @@ class SidebarRootViewsBloc
await event.when(
initial: (userProfile, workspaceId) async {
_initial(userProfile, workspaceId);
await _fetchApps(emit);
await _fetchRootViews(emit);
},
reset: (userProfile, workspaceId) async {
await _listener?.stop();
_initial(userProfile, workspaceId);
await _fetchApps(emit);
await _fetchRootViews(emit);
},
createRootView: (name, desc, index) async {
final result = await _workspaceService.createApp(
createRootView: (name, desc, index, section) async {
final result = await _workspaceService.createView(
name: name,
desc: desc,
index: index,
viewSection: section,
);
result.fold(
(view) => emit(state.copyWith(lastCreatedRootView: view)),
@ -59,48 +60,59 @@ class SidebarRootViewsBloc
);
},
didReceiveViews: (viewsOrFailure) async {
emit(
viewsOrFailure.fold(
(views) => state.copyWith(
views: views,
successOrFailure: FlowyResult.success(null),
),
(err) =>
state.copyWith(successOrFailure: FlowyResult.failure(err)),
),
);
// emit(
// viewsOrFailure.fold(
// (views) => state.copyWith(
// views: views,
// successOrFailure: FlowyResult.success(null),
// ),
// (err) =>
// state.copyWith(successOrFailure: FlowyResult.failure(err)),
// ),
// );
},
moveRootView: (int fromIndex, int toIndex) {
if (state.views.length > fromIndex) {
final view = state.views[fromIndex];
// if (state.views.length > fromIndex) {
// final view = state.views[fromIndex];
_workspaceService.moveApp(
appId: view.id,
fromIndex: fromIndex,
toIndex: toIndex,
);
// _workspaceService.moveApp(
// appId: view.id,
// fromIndex: fromIndex,
// toIndex: toIndex,
// );
final views = List<ViewPB>.from(state.views);
views.insert(toIndex, views.removeAt(fromIndex));
emit(state.copyWith(views: views));
}
// final views = List<ViewPB>.from(state.views);
// views.insert(toIndex, views.removeAt(fromIndex));
// emit(state.copyWith(views: views));
// }
},
);
},
);
}
Future<void> _fetchApps(Emitter<SidebarRootViewState> emit) async {
final viewsOrError = await _workspaceService.getViews();
Future<void> _fetchRootViews(
Emitter<SidebarRootViewState> emit,
) async {
try {
final publicViews = await _workspaceService.getPublicViews().getOrThrow();
final privateViews =
await _workspaceService.getPrivateViews().getOrThrow();
emit(
viewsOrError.fold(
(views) => state.copyWith(views: views),
(error) {
Log.error(error);
return state.copyWith(successOrFailure: FlowyResult.failure(error));
},
state.copyWith(
publicViews: publicViews,
privateViews: privateViews,
),
);
} catch (e) {
Log.error(e);
// TODO: handle error
// emit(
// state.copyWith(
// successOrFailure: FlowyResult.failure(e),
// ),
// );
}
}
void _handleAppsOrFail(FlowyResult<List<ViewPB>, FlowyError> viewsOrFail) {
@ -137,9 +149,12 @@ class SidebarRootViewsEvent with _$SidebarRootViewsEvent {
String name, {
String? desc,
int? index,
required ViewSectionPB viewSection,
}) = _createRootView;
const factory SidebarRootViewsEvent.moveRootView(int fromIndex, int toIndex) =
_MoveRootView;
const factory SidebarRootViewsEvent.moveRootView(
int fromIndex,
int toIndex,
) = _MoveRootView;
const factory SidebarRootViewsEvent.didReceiveViews(
FlowyResult<List<ViewPB>, FlowyError> appsOrFail,
) = _ReceiveApps;
@ -148,13 +163,13 @@ class SidebarRootViewsEvent with _$SidebarRootViewsEvent {
@freezed
class SidebarRootViewState with _$SidebarRootViewState {
const factory SidebarRootViewState({
required List<ViewPB> views,
@Default([]) List<ViewPB> privateViews,
@Default([]) List<ViewPB> publicViews,
required FlowyResult<void, FlowyError> successOrFailure,
@Default(null) ViewPB? lastCreatedRootView,
}) = _SidebarRootViewState;
factory SidebarRootViewState.initial() => SidebarRootViewState(
views: [],
successOrFailure: FlowyResult.success(null),
);
}

View File

@ -0,0 +1,261 @@
import 'dart:async';
import 'package:appflowy/workspace/application/workspace/workspace_sections_listener.dart';
import 'package:appflowy/workspace/application/workspace/workspace_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'sidebar_sections_bloc.freezed.dart';
class SidebarSection {
const SidebarSection({
required this.publicViews,
required this.privateViews,
});
const SidebarSection.empty()
: publicViews = const [],
privateViews = const [];
final List<ViewPB> publicViews;
final List<ViewPB> privateViews;
List<ViewPB> get views => publicViews + privateViews;
SidebarSection copyWith({
List<ViewPB>? publicViews,
List<ViewPB>? privateViews,
}) {
return SidebarSection(
publicViews: publicViews ?? this.publicViews,
privateViews: privateViews ?? this.privateViews,
);
}
}
/// The [SidebarSectionsBloc] is responsible for
/// managing the root views in different sections of the workspace.
class SidebarSectionsBloc
extends Bloc<SidebarSectionsEvent, SidebarSectionsState> {
SidebarSectionsBloc() : super(SidebarSectionsState.initial()) {
on<SidebarSectionsEvent>(
(event, emit) async {
await event.when(
initial: (userProfile, workspaceId) async {
_initial(userProfile, workspaceId);
final sectionViews = await _getSectionViews();
if (sectionViews != null) {
emit(
state.copyWith(
section: sectionViews,
),
);
}
},
reset: (userProfile, workspaceId) async {
_reset(userProfile, workspaceId);
final sectionViews = await _getSectionViews();
if (sectionViews != null) {
emit(
state.copyWith(
section: sectionViews,
),
);
}
},
createRootViewInSection: (name, section, desc, index) async {
final result = await _workspaceService.createView(
name: name,
viewSection: section,
desc: desc,
index: index,
);
result.fold(
(view) => emit(
state.copyWith(
lastCreatedRootView: view,
createRootViewResult: FlowyResult.success(null),
),
),
(error) {
Log.error('Failed to create root view: $error');
emit(
state.copyWith(
createRootViewResult: FlowyResult.failure(error),
),
);
},
);
},
receiveSectionViewsUpdate: (sectionViews) async {
final section = sectionViews.section;
switch (section) {
case ViewSectionPB.Public:
emit(
state.copyWith(
section: state.section.copyWith(
publicViews: sectionViews.views,
),
),
);
case ViewSectionPB.Private:
emit(
state.copyWith(
section: state.section.copyWith(
privateViews: sectionViews.views,
),
),
);
break;
default:
break;
}
},
moveRootView: (fromIndex, toIndex, fromSection, toSection) async {
final views = fromSection == ViewSectionPB.Public
? List<ViewPB>.from(state.section.publicViews)
: List<ViewPB>.from(state.section.privateViews);
if (fromIndex < 0 || fromIndex >= views.length) {
Log.error(
'Invalid fromIndex: $fromIndex, maxIndex: ${views.length - 1}',
);
return;
}
final view = views[fromIndex];
final result = await _workspaceService.moveView(
viewId: view.id,
fromIndex: fromIndex,
toIndex: toIndex,
);
result.fold(
(value) {
views.insert(toIndex, views.removeAt(fromIndex));
var newState = state;
if (fromSection == ViewSectionPB.Public) {
newState = newState.copyWith(
section: newState.section.copyWith(publicViews: views),
);
} else if (fromSection == ViewSectionPB.Private) {
newState = newState.copyWith(
section: newState.section.copyWith(privateViews: views),
);
}
emit(newState);
},
(error) {
Log.error('Failed to move root view: $error');
},
);
},
);
},
);
}
late WorkspaceService _workspaceService;
WorkspaceSectionsListener? _listener;
@override
Future<void> close() async {
await _listener?.stop();
_listener = null;
return super.close();
}
ViewSectionPB? getViewSection(ViewPB view) {
final publicViews = state.section.publicViews.map((e) => e.id);
final privateViews = state.section.privateViews.map((e) => e.id);
if (publicViews.contains(view.id)) {
return ViewSectionPB.Public;
} else if (privateViews.contains(view.id)) {
return ViewSectionPB.Private;
} else {
return null;
}
}
Future<SidebarSection?> _getSectionViews() async {
try {
final publicViews = await _workspaceService.getPublicViews().getOrThrow();
final privateViews =
await _workspaceService.getPrivateViews().getOrThrow();
return SidebarSection(
publicViews: publicViews,
privateViews: privateViews,
);
} catch (e) {
Log.error('Failed to get section views: $e');
return null;
}
}
void _initial(UserProfilePB userProfile, String workspaceId) {
_workspaceService = WorkspaceService(workspaceId: workspaceId);
_listener = WorkspaceSectionsListener(
user: userProfile,
workspaceId: workspaceId,
)..start(
sectionChanged: (result) {
if (!isClosed) {
result.fold(
(s) => add(SidebarSectionsEvent.receiveSectionViewsUpdate(s)),
(f) => Log.error('Failed to receive section views: $f'),
);
}
},
);
}
void _reset(UserProfilePB userProfile, String workspaceId) {
_listener?.stop();
_listener = null;
_initial(userProfile, workspaceId);
}
}
@freezed
class SidebarSectionsEvent with _$SidebarSectionsEvent {
const factory SidebarSectionsEvent.initial(
UserProfilePB userProfile,
String workspaceId,
) = _Initial;
const factory SidebarSectionsEvent.reset(
UserProfilePB userProfile,
String workspaceId,
) = _Reset;
const factory SidebarSectionsEvent.createRootViewInSection({
required String name,
required ViewSectionPB viewSection,
String? desc,
int? index,
}) = _CreateRootViewInSection;
const factory SidebarSectionsEvent.moveRootView({
required int fromIndex,
required int toIndex,
required ViewSectionPB fromSection,
required ViewSectionPB toSection,
}) = _MoveRootView;
const factory SidebarSectionsEvent.receiveSectionViewsUpdate(
SectionViewsPB sectionViews,
) = _ReceiveSectionViewsUpdate;
}
@freezed
class SidebarSectionsState with _$SidebarSectionsState {
const factory SidebarSectionsState({
required SidebarSection section,
@Default(null) ViewPB? lastCreatedRootView,
FlowyResult<void, FlowyError>? createRootViewResult,
}) = _SidebarSectionsState;
factory SidebarSectionsState.initial() => const SidebarSectionsState(
section: SidebarSection.empty(),
);
}

View File

@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@ -10,7 +11,19 @@ part 'folder_bloc.freezed.dart';
enum FolderCategoryType {
favorite,
personal,
private,
public;
ViewSectionPB get toViewSectionPB {
switch (this) {
case FolderCategoryType.private:
return ViewSectionPB.Private;
case FolderCategoryType.public:
return ViewSectionPB.Public;
case FolderCategoryType.favorite:
throw UnimplementedError();
}
}
}
class FolderBloc extends Bloc<FolderEvent, FolderState> {

View File

@ -2,7 +2,7 @@ import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@ -20,14 +20,20 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
(event, emit) async {
await event.when(
initial: () async {
// do nothing
add(const FetchWorkspaces());
},
workspacesReceived: (workspaceId) async {},
fetchWorkspaces: () async {
final result = await _fetchWorkspaces();
if (result != null) {
final members = await _userService
.getWorkspaceMembers(
result.$1.workspaceId,
)
.fold((s) => s.items.length, (f) => -1);
emit(
state.copyWith(
isCollaborativeWorkspace: members > 1,
currentWorkspace: result.$1,
workspaces: result.$2,
),
@ -258,7 +264,7 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
workspaces.firstWhere((e) => e.workspaceId == currentWorkspace.id);
return (currentWorkspaceInList, workspaces);
} catch (e) {
Log.error(e);
Log.error('fetch workspace error: $e');
return null;
}
}
@ -292,6 +298,7 @@ class UserWorkspaceState with _$UserWorkspaceState {
const factory UserWorkspaceState({
required UserWorkspacePB? currentWorkspace,
required List<UserWorkspacePB> workspaces,
@Default(false) bool isCollaborativeWorkspace,
@Default(null) FlowyResult<void, FlowyError>? createWorkspaceResult,
@Default(null) FlowyResult<void, FlowyError>? deleteWorkspaceResult,
@Default(null) FlowyResult<void, FlowyError>? openWorkspaceResult,

View File

@ -165,6 +165,8 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
viewId: value.from.id,
newParentId: value.newParentId,
prevViewId: value.prevId,
fromSection: value.fromSection,
toSection: value.toSection,
);
emit(
result.fold(
@ -184,8 +186,8 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
layoutType: e.layoutType,
ext: {},
openAfterCreate: e.openAfterCreated,
section: e.section,
);
emit(
result.fold(
(view) => state.copyWith(
@ -353,12 +355,15 @@ class ViewEvent with _$ViewEvent {
ViewPB from,
String newParentId,
String? prevId,
ViewSectionPB? fromSection,
ViewSectionPB? toSection,
) = Move;
const factory ViewEvent.createView(
String name,
ViewLayoutPB layoutType, {
/// open the view after created
@Default(true) bool openAfterCreated,
required ViewSectionPB section,
}) = CreateView;
const factory ViewEvent.viewDidUpdate(
FlowyResult<ViewPB, FlowyError> result,

View File

@ -37,6 +37,7 @@ class ViewBackendService {
/// The [index] is the index of the view in the parent view.
/// If the index is null, the view will be added to the end of the list.
int? index,
ViewSectionPB? section,
}) {
final payload = CreateViewPayloadPB.create()
..parentViewId = parentViewId
@ -58,6 +59,10 @@ class ViewBackendService {
payload.index = index;
}
if (section != null) {
payload.section = section;
}
return FolderEventCreateView(payload).send();
}
@ -195,11 +200,15 @@ class ViewBackendService {
required String viewId,
required String newParentId,
required String? prevViewId,
ViewSectionPB? fromSection,
ViewSectionPB? toSection,
}) {
final payload = MoveNestedViewPayloadPB(
viewId: viewId,
newParentId: newParentId,
prevViewId: prevViewId,
fromSection: fromSection,
toSection: toSection,
);
return FolderEventMoveNestedView(payload).send();

View File

@ -11,23 +11,28 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
import 'package:appflowy_result/appflowy_result.dart';
import 'package:flowy_infra/notifier.dart';
typedef AppListNotifyValue = FlowyResult<List<ViewPB>, FlowyError>;
typedef RootViewsNotifyValue = FlowyResult<List<ViewPB>, FlowyError>;
typedef WorkspaceNotifyValue = FlowyResult<WorkspacePB, FlowyError>;
/// The [WorkspaceListener] listens to the changes including the below:
///
/// - The root views of the workspace. (Not including the views are inside the root views)
/// - The workspace itself.
class WorkspaceListener {
WorkspaceListener({required this.user, required this.workspaceId});
final UserProfilePB user;
final String workspaceId;
PublishNotifier<AppListNotifyValue>? _appsChangedNotifier = PublishNotifier();
PublishNotifier<RootViewsNotifyValue>? _appsChangedNotifier =
PublishNotifier();
PublishNotifier<WorkspaceNotifyValue>? _workspaceUpdatedNotifier =
PublishNotifier();
FolderNotificationListener? _listener;
void start({
void Function(AppListNotifyValue)? appsChanged,
void Function(RootViewsNotifyValue)? appsChanged,
void Function(WorkspaceNotifyValue)? onWorkspaceUpdated,
}) {
if (appsChanged != null) {

View File

@ -0,0 +1,68 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:appflowy/core/notification/folder_notification.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/notification.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
show UserProfilePB;
import 'package:appflowy_result/appflowy_result.dart';
import 'package:flowy_infra/notifier.dart';
typedef SectionNotifyValue = FlowyResult<SectionViewsPB, FlowyError>;
/// The [WorkspaceSectionsListener] listens to the changes including the below:
///
/// - The root views inside different section of the workspace. (Not including the views are inside the root views)
/// depends on the section type(s).
class WorkspaceSectionsListener {
WorkspaceSectionsListener({
required this.user,
required this.workspaceId,
});
final UserProfilePB user;
final String workspaceId;
final _sectionNotifier = PublishNotifier<SectionNotifyValue>();
late final FolderNotificationListener _listener;
void start({
void Function(SectionNotifyValue)? sectionChanged,
}) {
if (sectionChanged != null) {
_sectionNotifier.addPublishListener(sectionChanged);
}
_listener = FolderNotificationListener(
objectId: workspaceId,
handler: _handleObservableType,
);
}
void _handleObservableType(
FolderNotification ty,
FlowyResult<Uint8List, FlowyError> result,
) {
switch (ty) {
case FolderNotification.DidUpdateSectionViews:
final FlowyResult<SectionViewsPB, FlowyError> value = result.fold(
(s) => FlowyResult.success(
SectionViewsPB.fromBuffer(s),
),
(f) => FlowyResult.failure(f),
);
_sectionNotifier.value = value;
break;
default:
break;
}
}
Future<void> stop() async {
_sectionNotifier.dispose();
await _listener.stop();
}
}

View File

@ -2,9 +2,7 @@ import 'dart:async';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'
show CreateViewPayloadPB, MoveViewPayloadPB, ViewLayoutPB, ViewPB;
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:appflowy_result/appflowy_result.dart';
class WorkspaceService {
@ -12,15 +10,18 @@ class WorkspaceService {
final String workspaceId;
Future<FlowyResult<ViewPB, FlowyError>> createApp({
Future<FlowyResult<ViewPB, FlowyError>> createView({
required String name,
required ViewSectionPB viewSection,
String? desc,
int? index,
}) {
final payload = CreateViewPayloadPB.create()
..parentViewId = workspaceId
..name = name
..layout = ViewLayoutPB.Document;
// only allow document layout for the top-level views
..layout = ViewLayoutPB.Document
..section = viewSection;
if (desc != null) {
payload.desc = desc;
@ -37,8 +38,8 @@ class WorkspaceService {
return FolderEventReadCurrentWorkspace().send();
}
Future<FlowyResult<List<ViewPB>, FlowyError>> getViews() {
final payload = WorkspaceIdPB.create()..value = workspaceId;
Future<FlowyResult<List<ViewPB>, FlowyError>> getPublicViews() {
final payload = GetWorkspaceViewPB.create()..value = workspaceId;
return FolderEventReadWorkspaceViews(payload).send().then((result) {
return result.fold(
(views) => FlowyResult.success(views.items),
@ -47,13 +48,23 @@ class WorkspaceService {
});
}
Future<FlowyResult<void, FlowyError>> moveApp({
required String appId,
Future<FlowyResult<List<ViewPB>, FlowyError>> getPrivateViews() {
final payload = GetWorkspaceViewPB.create()..value = workspaceId;
return FolderEventReadPrivateViews(payload).send().then((result) {
return result.fold(
(views) => FlowyResult.success(views.items),
(error) => FlowyResult.failure(error),
);
});
}
Future<FlowyResult<void, FlowyError>> moveView({
required String viewId,
required int fromIndex,
required int toIndex,
}) {
final payload = MoveViewPayloadPB.create()
..viewId = appId
..viewId = viewId
..from = fromIndex
..to = toIndex;

View File

@ -0,0 +1,63 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
class FolderHeader extends StatefulWidget {
const FolderHeader({
super.key,
required this.title,
required this.expandButtonTooltip,
required this.addButtonTooltip,
required this.onPressed,
required this.onAdded,
});
final String title;
final String expandButtonTooltip;
final String addButtonTooltip;
final VoidCallback onPressed;
final VoidCallback onAdded;
@override
State<FolderHeader> createState() => _FolderHeaderState();
}
class _FolderHeaderState extends State<FolderHeader> {
bool onHover = false;
@override
Widget build(BuildContext context) {
const iconSize = 26.0;
const textPadding = 4.0;
return MouseRegion(
onEnter: (event) => setState(() => onHover = true),
onExit: (event) => setState(() => onHover = false),
child: Row(
children: [
FlowyTextButton(
widget.title,
tooltip: widget.expandButtonTooltip,
constraints: const BoxConstraints(
minHeight: iconSize + textPadding * 2,
),
padding: const EdgeInsets.all(textPadding),
fillColor: Colors.transparent,
onPressed: widget.onPressed,
),
if (onHover) ...[
const Spacer(),
FlowyIconButton(
tooltipText: widget.addButtonTooltip,
hoverColor: Theme.of(context).colorScheme.secondaryContainer,
iconPadding: const EdgeInsets.all(2),
height: iconSize,
width: iconSize,
icon: const FlowySvg(FlowySvgs.add_s),
onPressed: widget.onAdded,
),
],
],
),
);
}
}

View File

@ -0,0 +1,116 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/_folder_header.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/rename_view_dialog.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class SectionFolder extends StatelessWidget {
const SectionFolder({
super.key,
required this.title,
required this.categoryType,
required this.views,
this.isHoverEnabled = true,
});
final String title;
final FolderCategoryType categoryType;
final List<ViewPB> views;
final bool isHoverEnabled;
@override
Widget build(BuildContext context) {
return BlocProvider<FolderBloc>(
create: (context) => FolderBloc(type: categoryType)
..add(
const FolderEvent.initial(),
),
child: BlocBuilder<FolderBloc, FolderState>(
builder: (context, state) {
return Column(
children: [
FolderHeader(
title: title,
expandButtonTooltip: expandButtonTooltip,
addButtonTooltip: addButtonTooltip,
onPressed: () => context
.read<FolderBloc>()
.add(const FolderEvent.expandOrUnExpand()),
onAdded: () {
createViewAndShowRenameDialogIfNeeded(
context,
LocaleKeys.newPageText.tr(),
(viewName, _) {
if (viewName.isNotEmpty) {
context.read<SidebarSectionsBloc>().add(
SidebarSectionsEvent.createRootViewInSection(
name: viewName,
index: 0,
viewSection: categoryType.toViewSectionPB,
),
);
context.read<FolderBloc>().add(
const FolderEvent.expandOrUnExpand(
isExpanded: true,
),
);
}
},
);
},
),
if (state.isExpanded)
...views.map(
(view) => ViewItem(
key: ValueKey(
'${categoryType.name} ${view.id}',
),
categoryType: categoryType,
isFirstChild: view.id == views.first.id,
view: view,
level: 0,
leftPadding: 16,
isFeedback: false,
onSelected: (view) {
if (HardwareKeyboard.instance.isControlPressed) {
context.read<TabsBloc>().openTab(view);
}
context.read<TabsBloc>().openPlugin(view);
},
onTertiarySelected: (view) =>
context.read<TabsBloc>().openTab(view),
isHoverEnabled: isHoverEnabled,
),
),
],
);
},
),
);
}
String get expandButtonTooltip {
return switch (categoryType) {
FolderCategoryType.public => LocaleKeys.sideBar_clickToHidePublic.tr(),
FolderCategoryType.private => LocaleKeys.sideBar_clickToHidePrivate.tr(),
_ => '',
};
}
String get addButtonTooltip {
return switch (categoryType) {
FolderCategoryType.public => LocaleKeys.sideBar_addAPageToPublic.tr(),
FolderCategoryType.private => LocaleKeys.sideBar_addAPageToPrivate.tr(),
_ => '',
};
}
}

View File

@ -1,146 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/rename_view_dialog.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class PersonalFolder extends StatelessWidget {
const PersonalFolder({
super.key,
required this.views,
this.isHoverEnabled = true,
});
final List<ViewPB> views;
final bool isHoverEnabled;
@override
Widget build(BuildContext context) {
return BlocProvider<FolderBloc>(
create: (context) => FolderBloc(type: FolderCategoryType.personal)
..add(
const FolderEvent.initial(),
),
child: BlocBuilder<FolderBloc, FolderState>(
builder: (context, state) {
return Column(
children: [
PersonalFolderHeader(
onPressed: () => context
.read<FolderBloc>()
.add(const FolderEvent.expandOrUnExpand()),
onAdded: () => context
.read<FolderBloc>()
.add(const FolderEvent.expandOrUnExpand(isExpanded: true)),
),
if (state.isExpanded)
...views.map(
(view) => ViewItem(
key: ValueKey(
'${FolderCategoryType.personal.name} ${view.id}',
),
categoryType: FolderCategoryType.personal,
isFirstChild: view.id == views.first.id,
view: view,
level: 0,
leftPadding: 16,
isFeedback: false,
onSelected: (view) {
if (HardwareKeyboard.instance.isControlPressed) {
context.read<TabsBloc>().openTab(view);
}
context.read<TabsBloc>().openPlugin(view);
},
onTertiarySelected: (view) =>
context.read<TabsBloc>().openTab(view),
isHoverEnabled: isHoverEnabled,
),
),
],
);
},
),
);
}
}
class PersonalFolderHeader extends StatefulWidget {
const PersonalFolderHeader({
super.key,
required this.onPressed,
required this.onAdded,
});
final VoidCallback onPressed;
final VoidCallback onAdded;
@override
State<PersonalFolderHeader> createState() => _PersonalFolderHeaderState();
}
class _PersonalFolderHeaderState extends State<PersonalFolderHeader> {
bool onHover = false;
@override
Widget build(BuildContext context) {
const iconSize = 26.0;
const textPadding = 4.0;
return MouseRegion(
onEnter: (event) => setState(() => onHover = true),
onExit: (event) => setState(() => onHover = false),
child: Row(
children: [
FlowyTextButton(
LocaleKeys.sideBar_personal.tr(),
tooltip: LocaleKeys.sideBar_clickToHidePersonal.tr(),
constraints: const BoxConstraints(
minHeight: iconSize + textPadding * 2,
),
padding: const EdgeInsets.all(textPadding),
fillColor: Colors.transparent,
onPressed: widget.onPressed,
),
if (onHover) ...[
const Spacer(),
FlowyIconButton(
tooltipText: LocaleKeys.sideBar_addAPage.tr(),
hoverColor: Theme.of(context).colorScheme.secondaryContainer,
iconPadding: const EdgeInsets.all(2),
height: iconSize,
width: iconSize,
icon: const FlowySvg(FlowySvgs.add_s),
onPressed: () {
createViewAndShowRenameDialogIfNeeded(
context,
LocaleKeys.newPageText.tr(),
(viewName, _) {
if (viewName.isNotEmpty) {
context.read<SidebarRootViewsBloc>().add(
SidebarRootViewsEvent.createRootView(
viewName,
index: 0,
),
);
widget.onAdded();
}
},
);
},
),
],
],
),
);
}
}

View File

@ -2,8 +2,7 @@ import 'dart:async';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart';
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
import 'package:appflowy/workspace/application/notifications/notification_action.dart';
import 'package:appflowy/workspace/application/notifications/notification_action_bloc.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
@ -15,8 +14,8 @@ import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_top_me
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_trash.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_user.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/auth.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
show UserProfilePB;
import 'package:appflowy_editor/appflowy_editor.dart';
@ -31,7 +30,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
/// - settings
/// - scrollable document list
/// - trash
class HomeSideBar extends StatefulWidget {
class HomeSideBar extends StatelessWidget {
const HomeSideBar({
super.key,
required this.userProfile,
@ -42,49 +41,30 @@ class HomeSideBar extends StatefulWidget {
final WorkspaceSettingPB workspaceSetting;
@override
State<HomeSideBar> createState() => _HomeSideBarState();
}
class _HomeSideBarState extends State<HomeSideBar> {
final _scrollController = ScrollController();
Timer? _srollDebounce;
bool isScrolling = false;
@override
void initState() {
super.initState();
_scrollController.addListener(_onScrollChanged);
}
void _onScrollChanged() {
setState(() => isScrolling = true);
_srollDebounce?.cancel();
_srollDebounce =
Timer(const Duration(milliseconds: 300), _setScrollStopped);
}
void _setScrollStopped() {
if (mounted) {
setState(() => isScrolling = false);
}
}
@override
void dispose() {
_srollDebounce?.cancel();
_scrollController.removeListener(_onScrollChanged);
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
// Workspace Bloc: control the current workspace
// |
// +-- Workspace Menu
// | |
// | +-- Workspace List: control to switch workspace
// | |
// | +-- Workspace Settings
// | |
// | +-- Notification Center
// |
// +-- Favorite Section
// |
// +-- Public Or Private Section: control the sections of the workspace
// |
// +-- Trash Section
return BlocProvider<UserWorkspaceBloc>(
create: (_) => UserWorkspaceBloc(userProfile: widget.userProfile)
..add(const UserWorkspaceEvent.fetchWorkspaces()),
create: (_) => UserWorkspaceBloc(userProfile: userProfile)
..add(
const UserWorkspaceEvent.initial(),
),
child: BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(
// Rebuild the whole sidebar when the current workspace changes
buildWhen: (previous, current) =>
previous.currentWorkspace?.workspaceId !=
current.currentWorkspace?.workspaceId,
@ -95,19 +75,19 @@ class _HomeSideBarState extends State<HomeSideBar> {
create: (_) => getIt<NotificationActionBloc>(),
),
BlocProvider(
create: (_) => SidebarRootViewsBloc()
create: (_) => SidebarSectionsBloc()
..add(
SidebarRootViewsEvent.initial(
widget.userProfile,
SidebarSectionsEvent.initial(
userProfile,
state.currentWorkspace?.workspaceId ??
widget.workspaceSetting.workspaceId,
workspaceSetting.workspaceId,
),
),
),
],
child: MultiBlocListener(
listeners: [
BlocListener<SidebarRootViewsBloc, SidebarRootViewState>(
BlocListener<SidebarSectionsBloc, SidebarSectionsState>(
listenWhen: (p, c) =>
p.lastCreatedRootView?.id != c.lastCreatedRootView?.id,
listener: (context, state) => context.read<TabsBloc>().add(
@ -122,28 +102,17 @@ class _HomeSideBarState extends State<HomeSideBar> {
),
BlocListener<UserWorkspaceBloc, UserWorkspaceState>(
listener: (context, state) {
context.read<SidebarRootViewsBloc>().add(
SidebarRootViewsEvent.reset(
widget.userProfile,
context.read<SidebarSectionsBloc>().add(
SidebarSectionsEvent.initial(
userProfile,
state.currentWorkspace?.workspaceId ??
widget.workspaceSetting.workspaceId,
workspaceSetting.workspaceId,
),
);
},
),
],
child: Builder(
builder: (context) {
final menuState = context.watch<SidebarRootViewsBloc>().state;
final favoriteState = context.watch<FavoriteBloc>().state;
return _buildSidebar(
context,
menuState.views,
favoriteState.views,
);
},
),
child: _Sidebar(userProfile: userProfile),
),
);
},
@ -151,71 +120,6 @@ class _HomeSideBarState extends State<HomeSideBar> {
);
}
Widget _buildSidebar(
BuildContext context,
List<ViewPB> views,
List<ViewPB> favoriteViews,
) {
const menuHorizontalInset = EdgeInsets.symmetric(horizontal: 12);
return DecoratedBox(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant,
border: Border(
right: BorderSide(color: Theme.of(context).dividerColor),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// top menu
const Padding(
padding: menuHorizontalInset,
child: SidebarTopMenu(),
),
// user or workspace, setting
Padding(
padding: menuHorizontalInset,
child: FeatureFlag.collaborativeWorkspace.isOn
? SidebarWorkspace(
userProfile: widget.userProfile,
views: views,
)
: SidebarUser(
userProfile: widget.userProfile,
views: views,
),
),
const VSpace(20),
// scrollable document list
Expanded(
child: Padding(
padding: menuHorizontalInset,
child: SingleChildScrollView(
controller: _scrollController,
physics: const ClampingScrollPhysics(),
child: SidebarFolder(
views: views,
favoriteViews: favoriteViews,
isHoverEnabled: !isScrolling,
),
),
),
),
const VSpace(10),
// trash
const Padding(
padding: menuHorizontalInset,
child: SidebarTrashButton(),
),
const VSpace(10),
// new page button
const SidebarNewPageButton(),
],
),
);
}
void _onNotificationAction(
BuildContext context,
NotificationActionState state,
@ -224,9 +128,10 @@ class _HomeSideBarState extends State<HomeSideBar> {
if (action != null) {
if (action.type == ActionType.openView) {
final view = context
.read<SidebarRootViewsBloc>()
.read<SidebarSectionsBloc>()
.state
.views
.section
.publicViews
.findView(action.objectId);
if (view != null) {
@ -250,3 +155,108 @@ class _HomeSideBarState extends State<HomeSideBar> {
}
}
}
class _Sidebar extends StatefulWidget {
const _Sidebar({
required this.userProfile,
});
final UserProfilePB userProfile;
@override
State<_Sidebar> createState() => _SidebarState();
}
class _SidebarState extends State<_Sidebar> {
final _scrollController = ScrollController();
Timer? _scrollDebounce;
bool isScrolling = false;
@override
void initState() {
super.initState();
_scrollController.addListener(_onScrollChanged);
}
@override
void dispose() {
_scrollDebounce?.cancel();
_scrollController.removeListener(_onScrollChanged);
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
const menuHorizontalInset = EdgeInsets.symmetric(horizontal: 12);
return DecoratedBox(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant,
border: Border(
right: BorderSide(color: Theme.of(context).dividerColor),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// top menu
const Padding(
padding: menuHorizontalInset,
child: SidebarTopMenu(),
),
// user or workspace, setting
Padding(
padding: menuHorizontalInset,
child: widget.userProfile.authenticator != AuthenticatorPB.Local &&
FeatureFlag.collaborativeWorkspace.isOn
? SidebarWorkspace(
userProfile: widget.userProfile,
)
: SidebarUser(
userProfile: widget.userProfile,
),
),
const VSpace(20),
// scrollable document list
Expanded(
child: Padding(
padding: menuHorizontalInset,
child: SingleChildScrollView(
controller: _scrollController,
physics: const ClampingScrollPhysics(),
child: SidebarFolder(
userProfile: widget.userProfile,
isHoverEnabled: !isScrolling,
),
),
),
),
const VSpace(10),
// trash
const Padding(
padding: menuHorizontalInset,
child: SidebarTrashButton(),
),
const VSpace(10),
// new page button
const SidebarNewPageButton(),
],
),
);
}
void _onScrollChanged() {
setState(() => isScrolling = true);
_scrollDebounce?.cancel();
_scrollDebounce =
Timer(const Duration(milliseconds: 300), _setScrollStopped);
}
void _setScrollStopped() {
if (mounted) {
setState(() => isScrolling = false);
}
}
}

View File

@ -1,50 +1,118 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/favorite_folder.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/_favorite_folder.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/_section_folder.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class SidebarFolder extends StatelessWidget {
const SidebarFolder({
super.key,
required this.views,
required this.favoriteViews,
this.isHoverEnabled = true,
required this.userProfile,
});
final List<ViewPB> views;
final List<ViewPB> favoriteViews;
final bool isHoverEnabled;
final UserProfilePB userProfile;
@override
Widget build(BuildContext context) {
// check if there is any duplicate views
final views = this.views.toSet().toList();
final favoriteViews = this.favoriteViews.toSet().toList();
assert(views.length == this.views.length);
assert(favoriteViews.length == favoriteViews.length);
return ValueListenableBuilder(
valueListenable: getIt<MenuSharedState>().notifier,
builder: (context, value, child) {
return Column(
children: [
// favorite
if (favoriteViews.isNotEmpty) ...[
FavoriteFolder(
BlocBuilder<FavoriteBloc, FavoriteState>(
builder: (context, state) {
if (state.views.isEmpty) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.only(bottom: 10),
child: FavoriteFolder(
// remove the duplicate views
views: favoriteViews,
views: state.views,
),
);
},
),
// public or private
BlocBuilder<SidebarSectionsBloc, SidebarSectionsState>(
builder: (context, state) {
// only show public and private section if the workspace is collaborative and not local
final isCollaborativeWorkspace =
userProfile.authenticator != AuthenticatorPB.Local &&
FeatureFlag.collaborativeWorkspace.isOn;
return Column(
children:
// only show public and private section if the workspace is collaborative
isCollaborativeWorkspace
? [
// public
const VSpace(10),
],
PublicSectionFolder(
views: state.section.publicViews,
),
// private
const VSpace(10),
PrivateSectionFolder(
views: state.section.privateViews,
),
]
: [
// personal
PersonalFolder(views: views, isHoverEnabled: isHoverEnabled),
const VSpace(10),
PersonalSectionFolder(
views: state.section.publicViews,
),
],
);
},
),
],
);
},
);
}
}
class PrivateSectionFolder extends SectionFolder {
PrivateSectionFolder({
super.key,
required super.views,
}) : super(
title: LocaleKeys.sideBar_private.tr(),
categoryType: FolderCategoryType.private,
);
}
class PublicSectionFolder extends SectionFolder {
PublicSectionFolder({
super.key,
required super.views,
}) : super(
title: LocaleKeys.sideBar_public.tr(),
categoryType: FolderCategoryType.public,
);
}
class PersonalSectionFolder extends SectionFolder {
PersonalSectionFolder({
super.key,
required super.views,
}) : super(
title: LocaleKeys.sideBar_personal.tr(),
categoryType: FolderCategoryType.public,
);
}

View File

@ -1,7 +1,8 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart';
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/rename_view_dialog.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/extension.dart';
@ -25,9 +26,12 @@ class SidebarNewPageButton extends StatelessWidget {
LocaleKeys.newPageText.tr(),
(viewName, _) {
if (viewName.isNotEmpty) {
context
.read<SidebarRootViewsBloc>()
.add(SidebarRootViewsEvent.createRootView(viewName));
context.read<SidebarSectionsBloc>().add(
SidebarSectionsEvent.createRootViewInSection(
name: viewName,
viewSection: ViewSectionPB.Public,
),
);
}
},
),

View File

@ -4,7 +4,7 @@ import 'package:appflowy/core/frameless_window.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/home/home_setting_bloc.dart';
import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart';
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
@ -24,7 +24,7 @@ class SidebarTopMenu extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<SidebarRootViewsBloc, SidebarRootViewState>(
return BlocBuilder<SidebarSectionsBloc, SidebarSectionsState>(
builder: (context, state) {
return SizedBox(
height: HomeSizes.topBarHeight,

View File

@ -3,7 +3,6 @@ import 'package:appflowy/workspace/application/menu/menu_user_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_setting.dart';
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_button.dart';
import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
show UserProfilePB;
import 'package:easy_localization/easy_localization.dart';
@ -17,11 +16,9 @@ class SidebarUser extends StatelessWidget {
const SidebarUser({
super.key,
required this.userProfile,
required this.views,
});
final UserProfilePB userProfile;
final List<ViewPB> views;
@override
Widget build(BuildContext context) {
@ -37,13 +34,13 @@ class SidebarUser extends StatelessWidget {
iconUrl: state.userProfile.iconUrl,
name: state.userProfile.name,
),
const HSpace(4),
const HSpace(8),
Expanded(
child: _buildUserName(context, state),
),
UserSettingButton(userProfile: state.userProfile),
const HSpace(4),
NotificationButton(views: views),
const NotificationButton(),
],
),
),

View File

@ -6,7 +6,6 @@ import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sid
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_button.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
@ -20,11 +19,9 @@ class SidebarWorkspace extends StatelessWidget {
const SidebarWorkspace({
super.key,
required this.userProfile,
required this.views,
});
final UserProfilePB userProfile;
final List<ViewPB> views;
@override
Widget build(BuildContext context) {
@ -46,7 +43,7 @@ class SidebarWorkspace extends StatelessWidget {
),
UserSettingButton(userProfile: userProfile),
const HSpace(4),
NotificationButton(views: views),
const NotificationButton(),
],
);
},

View File

@ -182,13 +182,13 @@ class WorkspaceMenuItem extends StatelessWidget {
Widget _buildRightIcon(BuildContext context) {
// only the owner can update or delete workspace.
// only show the more action button when the workspace is selected.
if (!isSelected ||
!context.read<WorkspaceMemberBloc>().state.myRole.isOwner) {
if (!isSelected) {
return const SizedBox.shrink();
}
return Row(
children: [
if (context.read<WorkspaceMemberBloc>().state.myRole.isOwner)
WorkspaceMoreActionList(workspace: workspace),
const FlowySvg(
FlowySvgs.blue_check_s,

View File

@ -1,3 +1,4 @@
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/widgets/draggable_item/draggable_item.dart';
@ -188,6 +189,9 @@ class _DraggableViewItemState extends State<DraggableViewItem> {
return;
}
final fromSection = getViewSection(from);
final toSection = getViewSection(to);
switch (position) {
case DraggableHoverPosition.top:
context.read<ViewBloc>().add(
@ -195,6 +199,8 @@ class _DraggableViewItemState extends State<DraggableViewItem> {
from,
to.parentViewId,
null,
fromSection,
toSection,
),
);
break;
@ -204,6 +210,8 @@ class _DraggableViewItemState extends State<DraggableViewItem> {
from,
to.parentViewId,
to.id,
fromSection,
toSection,
),
);
break;
@ -213,6 +221,8 @@ class _DraggableViewItemState extends State<DraggableViewItem> {
from,
to.id,
to.childViews.lastOrNull?.id,
fromSection,
toSection,
),
);
break;
@ -251,6 +261,10 @@ class _DraggableViewItemState extends State<DraggableViewItem> {
return true;
}
ViewSectionPB? getViewSection(ViewPB view) {
return context.read<SidebarSectionsBloc>().getViewSection(view);
}
}
extension on ViewPB {

View File

@ -475,6 +475,7 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
viewName,
pluginBuilder.layoutType!,
openAfterCreated: openAfterCreated,
section: widget.categoryType.toViewSectionPB,
),
);
}

View File

@ -2,8 +2,8 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
import 'package:appflowy/workspace/presentation/notifications/notification_dialog.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
@ -13,12 +13,13 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class NotificationButton extends StatelessWidget {
const NotificationButton({super.key, required this.views});
final List<ViewPB> views;
const NotificationButton({
super.key,
});
@override
Widget build(BuildContext context) {
final views = context.watch<SidebarSectionsBloc>().state.section.views;
final mutex = PopoverMutex();
return BlocProvider<ReminderBloc>.value(

View File

@ -1,6 +1,8 @@
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:collection/collection.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@ -21,7 +23,8 @@ class WorkspaceMemberBloc
WorkspaceMemberBloc({
required this.userProfile,
this.workspace,
}) : super(WorkspaceMemberState.initial()) {
}) : _userBackendService = UserBackendService(userId: userProfile.id),
super(WorkspaceMemberState.initial()) {
on<WorkspaceMemberEvent>((event, emit) async {
await event.when(
initial: () async {
@ -73,14 +76,16 @@ class WorkspaceMemberBloc
final UserWorkspacePB? workspace;
late final String workspaceId;
late final UserBackendService _userBackendService;
Future<List<WorkspaceMemberPB>> _getWorkspaceMembers() async {
final data = QueryWorkspacePB()..workspaceId = workspaceId;
final result = await UserEventGetWorkspaceMember(data).send();
return result.fold((s) => s.items, (e) {
return _userBackendService.getWorkspaceMembers(workspaceId).fold(
(s) => s.items,
(e) {
Log.error('Failed to read workspace members: $e');
return [];
});
},
);
}
AFRolePB _getMyRole(List<WorkspaceMemberPB> members) {
@ -97,40 +102,26 @@ class WorkspaceMemberBloc
}
Future<void> _addWorkspaceMember(String email) async {
final data = AddWorkspaceMemberPB()
..workspaceId = workspaceId
..email = email;
final result = await UserEventAddWorkspaceMember(data).send();
result.fold((s) {
Log.info('Added workspace member: $data');
}, (e) {
Log.error('Failed to add workspace member: $e');
});
return _userBackendService.addWorkspaceMember(workspaceId, email).fold(
(s) => Log.debug('Added workspace member: $email'),
(e) => Log.error('Failed to add workspace member: $e'),
);
}
Future<void> _removeWorkspaceMember(String email) async {
final data = RemoveWorkspaceMemberPB()
..workspaceId = workspaceId
..email = email;
final result = await UserEventRemoveWorkspaceMember(data).send();
result.fold((s) {
Log.info('Removed workspace member: $data');
}, (e) {
Log.error('Failed to remove workspace member: $e');
});
return _userBackendService.removeWorkspaceMember(workspaceId, email).fold(
(s) => Log.debug('Removed workspace member: $email'),
(e) => Log.error('Failed to remove workspace member: $e'),
);
}
Future<void> _updateWorkspaceMember(String email, AFRolePB role) async {
final data = UpdateWorkspaceMemberPB()
..workspaceId = workspaceId
..email = email
..role = role;
final result = await UserEventUpdateWorkspaceMember(data).send();
result.fold((s) {
Log.info('Updated workspace member: $data');
}, (e) {
Log.error('Failed to update workspace member: $e');
});
return _userBackendService
.updateWorkspaceMember(workspaceId, email, role)
.fold(
(s) => Log.debug('Updated workspace member: $email'),
(e) => Log.error('Failed to update workspace member: $e'),
);
}
}

View File

@ -50,7 +50,7 @@ class UserAvatar extends StatelessWidget {
),
child: FlowyText.semibold(
nameInitials,
color: Colors.white,
color: Colors.black,
fontSize: isLarge
? nameInitials.length == initialsCount
? 20

View File

@ -8,6 +8,10 @@ extension FlowyAsyncResultExtension<S, F extends Object>
return then((result) => result.getOrElse(onFailure));
}
Future<S?> toNullable() {
return then((result) => result.toNullable());
}
Future<S> getOrThrow() {
return then((result) => result.getOrThrow());
}

View File

@ -38,8 +38,13 @@ void main() {
final appBloc = ViewBloc(view: app)..add(const ViewEvent.initial());
assert(appBloc.state.lastCreatedView == null);
appBloc
.add(const ViewEvent.createView("New document", ViewLayoutPB.Document));
appBloc.add(
const ViewEvent.createView(
"New document",
ViewLayoutPB.Document,
section: ViewSectionPB.Public,
),
);
await blocResponseFuture();
assert(appBloc.state.lastCreatedView != null);

View File

@ -1,46 +0,0 @@
import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../util.dart';
void main() {
late AppFlowyUnitTest testContext;
setUpAll(() async {
testContext = await AppFlowyUnitTest.ensureInitialized();
});
test('assert initial apps is the build-in app', () async {
final menuBloc = SidebarRootViewsBloc()
..add(
SidebarRootViewsEvent.initial(
testContext.userProfile,
testContext.currentWorkspace.id,
),
);
await blocResponseFuture();
assert(menuBloc.state.views.length == 1);
});
test('reorder apps', () async {
final menuBloc = SidebarRootViewsBloc()
..add(
SidebarRootViewsEvent.initial(
testContext.userProfile,
testContext.currentWorkspace.id,
),
);
await blocResponseFuture();
menuBloc.add(const SidebarRootViewsEvent.createRootView("App 1"));
await blocResponseFuture();
menuBloc.add(const SidebarRootViewsEvent.createRootView("App 2"));
await blocResponseFuture();
menuBloc.add(const SidebarRootViewsEvent.createRootView("App 3"));
await blocResponseFuture();
assert(menuBloc.state.views[1].name == 'App 1');
assert(menuBloc.state.views[2].name == 'App 2');
assert(menuBloc.state.views[3].name == 'App 3');
});
}

View File

@ -0,0 +1,57 @@
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../util.dart';
void main() {
late AppFlowyUnitTest testContext;
setUpAll(() async {
testContext = await AppFlowyUnitTest.ensureInitialized();
});
test('assert initial apps is the build-in app', () async {
final menuBloc = SidebarSectionsBloc()
..add(
SidebarSectionsEvent.initial(
testContext.userProfile,
testContext.currentWorkspace.id,
),
);
await blocResponseFuture();
assert(menuBloc.state.section.publicViews.length == 1);
assert(menuBloc.state.section.privateViews.isEmpty);
});
test('create views', () async {
final menuBloc = SidebarSectionsBloc()
..add(
SidebarSectionsEvent.initial(
testContext.userProfile,
testContext.currentWorkspace.id,
),
);
await blocResponseFuture();
final names = ['View 1', 'View 2', 'View 3'];
for (final name in names) {
menuBloc.add(
SidebarSectionsEvent.createRootViewInSection(
name: name,
index: 0,
viewSection: ViewSectionPB.Public,
),
);
await blocResponseFuture();
}
final reversedNames = names.reversed.toList();
for (var i = 0; i < names.length; i++) {
assert(
menuBloc.state.section.publicViews[i].name == reversedNames[i],
);
}
});
}

View File

@ -22,6 +22,7 @@ class TrashTestContext {
const ViewEvent.createView(
"Document 1",
ViewLayoutPB.Document,
section: ViewSectionPB.Public,
),
);
await blocResponseFuture();
@ -30,6 +31,7 @@ class TrashTestContext {
const ViewEvent.createView(
"Document 2",
ViewLayoutPB.Document,
section: ViewSectionPB.Public,
),
);
await blocResponseFuture();
@ -38,6 +40,7 @@ class TrashTestContext {
const ViewEvent.createView(
"Document 3",
ViewLayoutPB.Document,
section: ViewSectionPB.Public,
),
);
await blocResponseFuture();

View File

@ -36,7 +36,11 @@ void main() {
final viewBloc = await createTestViewBloc();
// create a nested view
viewBloc.add(
const ViewEvent.createView(name, ViewLayoutPB.Document),
const ViewEvent.createView(
name,
ViewLayoutPB.Document,
section: ViewSectionPB.Public,
),
);
await blocResponseFuture();
expect(viewBloc.state.view.childViews.length, 1);
@ -52,7 +56,11 @@ void main() {
test('delete view test', () async {
final viewBloc = await createTestViewBloc();
viewBloc.add(
const ViewEvent.createView(name, ViewLayoutPB.Document),
const ViewEvent.createView(
name,
ViewLayoutPB.Document,
section: ViewSectionPB.Public,
),
);
await blocResponseFuture();
expect(viewBloc.state.view.childViews.length, 1);
@ -69,7 +77,11 @@ void main() {
test('create nested view test', () async {
final viewBloc = await createTestViewBloc();
viewBloc.add(
const ViewEvent.createView('Document 1', ViewLayoutPB.Document),
const ViewEvent.createView(
'Document 1',
ViewLayoutPB.Document,
section: ViewSectionPB.Public,
),
);
await blocResponseFuture();
final document1Bloc = ViewBloc(view: viewBloc.state.view.childViews.first)
@ -79,7 +91,11 @@ void main() {
await blocResponseFuture();
const name = 'Document 1 - 1';
document1Bloc.add(
const ViewEvent.createView('Document 1 - 1', ViewLayoutPB.Document),
const ViewEvent.createView(
'Document 1 - 1',
ViewLayoutPB.Document,
section: ViewSectionPB.Public,
),
);
await blocResponseFuture();
expect(document1Bloc.state.view.childViews.length, 1);
@ -91,7 +107,11 @@ void main() {
final names = ['1', '2', '3'];
for (final name in names) {
viewBloc.add(
ViewEvent.createView(name, ViewLayoutPB.Document),
ViewEvent.createView(
name,
ViewLayoutPB.Document,
section: ViewSectionPB.Public,
),
);
await blocResponseFuture();
}
@ -106,7 +126,13 @@ void main() {
final viewBloc = await createTestViewBloc();
expect(viewBloc.state.lastCreatedView, isNull);
viewBloc.add(const ViewEvent.createView('1', ViewLayoutPB.Document));
viewBloc.add(
const ViewEvent.createView(
'1',
ViewLayoutPB.Document,
section: ViewSectionPB.Public,
),
);
await blocResponseFuture();
expect(
viewBloc.state.lastCreatedView!.id,
@ -117,7 +143,13 @@ void main() {
'1',
);
viewBloc.add(const ViewEvent.createView('2', ViewLayoutPB.Document));
viewBloc.add(
const ViewEvent.createView(
'2',
ViewLayoutPB.Document,
section: ViewSectionPB.Public,
),
);
await blocResponseFuture();
expect(
viewBloc.state.lastCreatedView!.name,
@ -128,13 +160,25 @@ void main() {
test('open latest document test', () async {
const name1 = 'document';
final viewBloc = await createTestViewBloc();
viewBloc.add(const ViewEvent.createView(name1, ViewLayoutPB.Document));
viewBloc.add(
const ViewEvent.createView(
name1,
ViewLayoutPB.Document,
section: ViewSectionPB.Public,
),
);
await blocResponseFuture();
final document = viewBloc.state.lastCreatedView!;
assert(document.name == name1);
const gird = 'grid';
viewBloc.add(const ViewEvent.createView(gird, ViewLayoutPB.Document));
viewBloc.add(
const ViewEvent.createView(
gird,
ViewLayoutPB.Document,
section: ViewSectionPB.Public,
),
);
await blocResponseFuture();
assert(viewBloc.state.lastCreatedView!.name == gird);
@ -170,7 +214,11 @@ void main() {
for (var i = 0; i < layouts.length; i++) {
final layout = layouts[i];
viewBloc.add(
ViewEvent.createView('Test $layout', layout),
ViewEvent.createView(
'Test $layout',
layout,
section: ViewSectionPB.Public,
),
);
await blocResponseFuture();
expect(viewBloc.state.view.childViews.length, i + 1);

View File

@ -74,7 +74,10 @@ class AppFlowyUnitTest {
}
Future<ViewPB> createWorkspace() async {
final result = await workspaceService.createApp(name: "Test App");
final result = await workspaceService.createView(
name: "Test App",
viewSection: ViewSectionPB.Public,
);
return result.fold(
(app) => app,
(error) => throw Exception(error),
@ -82,7 +85,7 @@ class AppFlowyUnitTest {
}
Future<List<ViewPB>> loadApps() async {
final result = await workspaceService.getViews();
final result = await workspaceService.getPublicViews();
return result.fold(
(apps) => apps,

View File

@ -162,7 +162,7 @@ checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
[[package]]
name = "app-error"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325"
dependencies = [
"anyhow",
"bincode",
@ -716,7 +716,7 @@ dependencies = [
[[package]]
name = "client-api"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325"
dependencies = [
"again",
"anyhow",
@ -764,7 +764,7 @@ dependencies = [
[[package]]
name = "client-websocket"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325"
dependencies = [
"futures-channel",
"futures-util",
@ -838,7 +838,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2d7b1838e463ce0348cf700ff43f33f5718203be#2d7b1838e463ce0348cf700ff43f33f5718203be"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7"
dependencies = [
"anyhow",
"async-trait",
@ -854,6 +854,7 @@ dependencies = [
"tokio",
"tokio-stream",
"tracing",
"unicode-segmentation",
"web-sys",
"yrs",
]
@ -861,7 +862,7 @@ dependencies = [
[[package]]
name = "collab-database"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2d7b1838e463ce0348cf700ff43f33f5718203be#2d7b1838e463ce0348cf700ff43f33f5718203be"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7"
dependencies = [
"anyhow",
"async-trait",
@ -891,7 +892,7 @@ dependencies = [
[[package]]
name = "collab-document"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2d7b1838e463ce0348cf700ff43f33f5718203be#2d7b1838e463ce0348cf700ff43f33f5718203be"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7"
dependencies = [
"anyhow",
"collab",
@ -910,7 +911,7 @@ dependencies = [
[[package]]
name = "collab-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2d7b1838e463ce0348cf700ff43f33f5718203be#2d7b1838e463ce0348cf700ff43f33f5718203be"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7"
dependencies = [
"anyhow",
"bytes",
@ -925,7 +926,7 @@ dependencies = [
[[package]]
name = "collab-folder"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2d7b1838e463ce0348cf700ff43f33f5718203be#2d7b1838e463ce0348cf700ff43f33f5718203be"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7"
dependencies = [
"anyhow",
"chrono",
@ -962,7 +963,7 @@ dependencies = [
[[package]]
name = "collab-plugins"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2d7b1838e463ce0348cf700ff43f33f5718203be#2d7b1838e463ce0348cf700ff43f33f5718203be"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7"
dependencies = [
"anyhow",
"async-stream",
@ -1001,7 +1002,7 @@ dependencies = [
[[package]]
name = "collab-user"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2d7b1838e463ce0348cf700ff43f33f5718203be#2d7b1838e463ce0348cf700ff43f33f5718203be"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7"
dependencies = [
"anyhow",
"collab",
@ -1335,7 +1336,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]]
name = "database-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325"
dependencies = [
"anyhow",
"app-error",
@ -2637,7 +2638,7 @@ dependencies = [
[[package]]
name = "gotrue"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325"
dependencies = [
"anyhow",
"futures-util",
@ -2654,7 +2655,7 @@ dependencies = [
[[package]]
name = "gotrue-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325"
dependencies = [
"anyhow",
"app-error",
@ -3109,7 +3110,7 @@ dependencies = [
[[package]]
name = "infra"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325"
dependencies = [
"anyhow",
"reqwest",
@ -4892,7 +4893,7 @@ dependencies = [
[[package]]
name = "realtime-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325"
dependencies = [
"anyhow",
"bincode",
@ -4916,7 +4917,7 @@ dependencies = [
[[package]]
name = "realtime-protocol"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325"
dependencies = [
"anyhow",
"bincode",
@ -5588,7 +5589,7 @@ dependencies = [
[[package]]
name = "shared-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325"
dependencies = [
"anyhow",
"app-error",
@ -7551,7 +7552,7 @@ dependencies = [
[[package]]
name = "workspace-template"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325"
dependencies = [
"anyhow",
"async-trait",

View File

@ -86,7 +86,7 @@ custom-protocol = ["tauri/custom-protocol"]
# Run the script:
# scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c5112cc761736ac91f0a518552e7bbe522bceae6" }
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "ab9496c248b7c733d1aa160062abeb66c4e41325" }
# Please use the following script to update collab.
# Working directory: frontend
#
@ -96,10 +96,10 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c51
# To switch to the local path, run:
# scripts/tool/update_collab_source.sh
# ⚠️⚠️⚠️️
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2d7b1838e463ce0348cf700ff43f33f5718203be" }
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2d7b1838e463ce0348cf700ff43f33f5718203be" }
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2d7b1838e463ce0348cf700ff43f33f5718203be" }
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2d7b1838e463ce0348cf700ff43f33f5718203be" }
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2d7b1838e463ce0348cf700ff43f33f5718203be" }
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2d7b1838e463ce0348cf700ff43f33f5718203be" }
collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2d7b1838e463ce0348cf700ff43f33f5718203be" }
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" }
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" }
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" }
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" }
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" }
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" }
collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" }

View File

@ -1,16 +1,12 @@
import { parserViewPBToPage } from '$app_reducers/pages/slice';
import {
ChangeWorkspaceIconPB,
CreateViewPayloadPB,
GetWorkspaceViewPB,
RenameWorkspacePB,
UserWorkspaceIdPB,
WorkspaceIdPB,
RenameWorkspacePB,
ChangeWorkspaceIconPB,
} from '@/services/backend';
import {
UserEventOpenWorkspace,
UserEventRenameWorkspace,
UserEventChangeWorkspaceIcon,
UserEventGetAllWorkspace,
} from '@/services/backend/events/flowy-user';
import {
FolderEventCreateView,
FolderEventDeleteWorkspace,
@ -18,7 +14,12 @@ import {
FolderEventReadCurrentWorkspace,
FolderEventReadWorkspaceViews,
} from '@/services/backend/events/flowy-folder';
import { parserViewPBToPage } from '$app_reducers/pages/slice';
import {
UserEventChangeWorkspaceIcon,
UserEventGetAllWorkspace,
UserEventOpenWorkspace,
UserEventRenameWorkspace,
} from '@/services/backend/events/flowy-user';
export async function openWorkspace(id: string) {
const payload = new UserWorkspaceIdPB({
@ -49,7 +50,7 @@ export async function deleteWorkspace(id: string) {
}
export async function getWorkspaceChildViews(id: string) {
const payload = new WorkspaceIdPB({
const payload = new GetWorkspaceViewPB({
value: id,
});

View File

@ -221,7 +221,7 @@ checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
[[package]]
name = "app-error"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325"
dependencies = [
"anyhow",
"bincode",
@ -545,7 +545,7 @@ dependencies = [
[[package]]
name = "client-api"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325"
dependencies = [
"again",
"anyhow",
@ -592,7 +592,7 @@ dependencies = [
[[package]]
name = "client-websocket"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325"
dependencies = [
"futures-channel",
"futures-util",
@ -636,7 +636,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2d7b1838e463ce0348cf700ff43f33f5718203be#2d7b1838e463ce0348cf700ff43f33f5718203be"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7"
dependencies = [
"anyhow",
"async-trait",
@ -652,6 +652,7 @@ dependencies = [
"tokio",
"tokio-stream",
"tracing",
"unicode-segmentation",
"web-sys",
"yrs",
]
@ -659,7 +660,7 @@ dependencies = [
[[package]]
name = "collab-document"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2d7b1838e463ce0348cf700ff43f33f5718203be#2d7b1838e463ce0348cf700ff43f33f5718203be"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7"
dependencies = [
"anyhow",
"collab",
@ -678,7 +679,7 @@ dependencies = [
[[package]]
name = "collab-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2d7b1838e463ce0348cf700ff43f33f5718203be#2d7b1838e463ce0348cf700ff43f33f5718203be"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7"
dependencies = [
"anyhow",
"bytes",
@ -693,7 +694,7 @@ dependencies = [
[[package]]
name = "collab-folder"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2d7b1838e463ce0348cf700ff43f33f5718203be#2d7b1838e463ce0348cf700ff43f33f5718203be"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7"
dependencies = [
"anyhow",
"chrono",
@ -730,7 +731,7 @@ dependencies = [
[[package]]
name = "collab-plugins"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2d7b1838e463ce0348cf700ff43f33f5718203be#2d7b1838e463ce0348cf700ff43f33f5718203be"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7"
dependencies = [
"anyhow",
"async-stream",
@ -768,7 +769,7 @@ dependencies = [
[[package]]
name = "collab-user"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2d7b1838e463ce0348cf700ff43f33f5718203be#2d7b1838e463ce0348cf700ff43f33f5718203be"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7"
dependencies = [
"anyhow",
"collab",
@ -965,7 +966,7 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
[[package]]
name = "database-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325"
dependencies = [
"anyhow",
"app-error",
@ -1720,7 +1721,7 @@ dependencies = [
[[package]]
name = "gotrue"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325"
dependencies = [
"anyhow",
"futures-util",
@ -1737,7 +1738,7 @@ dependencies = [
[[package]]
name = "gotrue-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325"
dependencies = [
"anyhow",
"app-error",
@ -2071,7 +2072,7 @@ dependencies = [
[[package]]
name = "infra"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325"
dependencies = [
"anyhow",
"reqwest",
@ -3315,7 +3316,7 @@ dependencies = [
[[package]]
name = "realtime-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325"
dependencies = [
"anyhow",
"bincode",
@ -3339,7 +3340,7 @@ dependencies = [
[[package]]
name = "realtime-protocol"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325"
dependencies = [
"anyhow",
"bincode",
@ -3792,7 +3793,7 @@ dependencies = [
[[package]]
name = "shared-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325"
dependencies = [
"anyhow",
"app-error",
@ -5024,4 +5025,4 @@ dependencies = [
[[patch.unused]]
name = "collab-database"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2d7b1838e463ce0348cf700ff43f33f5718203be#2d7b1838e463ce0348cf700ff43f33f5718203be"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7"

View File

@ -55,7 +55,7 @@ codegen-units = 1
# Run the script:
# scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c5112cc761736ac91f0a518552e7bbe522bceae6" }
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "ab9496c248b7c733d1aa160062abeb66c4e41325" }
# Please use the following script to update collab.
# Working directory: frontend
#
@ -65,10 +65,10 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c51
# To switch to the local path, run:
# scripts/tool/update_collab_source.sh
# ⚠️⚠️⚠️️
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2d7b1838e463ce0348cf700ff43f33f5718203be" }
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2d7b1838e463ce0348cf700ff43f33f5718203be" }
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2d7b1838e463ce0348cf700ff43f33f5718203be" }
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2d7b1838e463ce0348cf700ff43f33f5718203be" }
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2d7b1838e463ce0348cf700ff43f33f5718203be" }
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2d7b1838e463ce0348cf700ff43f33f5718203be" }
collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2d7b1838e463ce0348cf700ff43f33f5718203be" }
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" }
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" }
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" }
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" }
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" }
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" }
collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" }

View File

@ -205,10 +205,16 @@
"closeSidebar": "Close side bar",
"openSidebar": "Open side bar",
"personal": "Personal",
"private": "Private",
"public": "Public",
"favorites": "Favorites",
"clickToHidePrivate": "Click to hide private section\nPages you created here are only visible to you",
"clickToHidePublic": "Click to hide public section\nPages you created here are visible to every member",
"clickToHidePersonal": "Click to hide personal section",
"clickToHideFavorites": "Click to hide favorite section",
"addAPage": "Add a page",
"addAPageToPrivate": "Add a page to private section",
"addAPageToPublic": "Add a page to public section",
"recent": "Recent"
},
"notifications": {

View File

@ -163,7 +163,7 @@ checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
[[package]]
name = "app-error"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325"
dependencies = [
"anyhow",
"bincode",
@ -673,7 +673,7 @@ dependencies = [
[[package]]
name = "client-api"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325"
dependencies = [
"again",
"anyhow",
@ -721,7 +721,7 @@ dependencies = [
[[package]]
name = "client-websocket"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325"
dependencies = [
"futures-channel",
"futures-util",
@ -764,7 +764,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2d7b1838e463ce0348cf700ff43f33f5718203be#2d7b1838e463ce0348cf700ff43f33f5718203be"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7"
dependencies = [
"anyhow",
"async-trait",
@ -780,6 +780,7 @@ dependencies = [
"tokio",
"tokio-stream",
"tracing",
"unicode-segmentation",
"web-sys",
"yrs",
]
@ -787,7 +788,7 @@ dependencies = [
[[package]]
name = "collab-database"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2d7b1838e463ce0348cf700ff43f33f5718203be#2d7b1838e463ce0348cf700ff43f33f5718203be"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7"
dependencies = [
"anyhow",
"async-trait",
@ -817,7 +818,7 @@ dependencies = [
[[package]]
name = "collab-document"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2d7b1838e463ce0348cf700ff43f33f5718203be#2d7b1838e463ce0348cf700ff43f33f5718203be"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7"
dependencies = [
"anyhow",
"collab",
@ -836,7 +837,7 @@ dependencies = [
[[package]]
name = "collab-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2d7b1838e463ce0348cf700ff43f33f5718203be#2d7b1838e463ce0348cf700ff43f33f5718203be"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7"
dependencies = [
"anyhow",
"bytes",
@ -851,7 +852,7 @@ dependencies = [
[[package]]
name = "collab-folder"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2d7b1838e463ce0348cf700ff43f33f5718203be#2d7b1838e463ce0348cf700ff43f33f5718203be"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7"
dependencies = [
"anyhow",
"chrono",
@ -888,7 +889,7 @@ dependencies = [
[[package]]
name = "collab-plugins"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2d7b1838e463ce0348cf700ff43f33f5718203be#2d7b1838e463ce0348cf700ff43f33f5718203be"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7"
dependencies = [
"anyhow",
"async-stream",
@ -927,7 +928,7 @@ dependencies = [
[[package]]
name = "collab-user"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2d7b1838e463ce0348cf700ff43f33f5718203be#2d7b1838e463ce0348cf700ff43f33f5718203be"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7"
dependencies = [
"anyhow",
"collab",
@ -1257,7 +1258,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]]
name = "database-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325"
dependencies = [
"anyhow",
"app-error",
@ -2432,7 +2433,7 @@ dependencies = [
[[package]]
name = "gotrue"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325"
dependencies = [
"anyhow",
"futures-util",
@ -2449,7 +2450,7 @@ dependencies = [
[[package]]
name = "gotrue-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325"
dependencies = [
"anyhow",
"app-error",
@ -2843,7 +2844,7 @@ dependencies = [
[[package]]
name = "infra"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325"
dependencies = [
"anyhow",
"reqwest",
@ -4325,7 +4326,7 @@ dependencies = [
[[package]]
name = "realtime-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325"
dependencies = [
"anyhow",
"bincode",
@ -4349,7 +4350,7 @@ dependencies = [
[[package]]
name = "realtime-protocol"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325"
dependencies = [
"anyhow",
"bincode",
@ -4942,7 +4943,7 @@ dependencies = [
[[package]]
name = "shared-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325"
dependencies = [
"anyhow",
"app-error",
@ -6355,7 +6356,7 @@ dependencies = [
[[package]]
name = "workspace-template"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325"
dependencies = [
"anyhow",
"async-trait",

View File

@ -110,7 +110,7 @@ rocksdb = { git = "https://github.com/LucasXu0/rust-rocksdb", rev = "21cf4a23ec1
# Run the script:
# scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c5112cc761736ac91f0a518552e7bbe522bceae6" }
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "ab9496c248b7c733d1aa160062abeb66c4e41325" }
# Please use the following script to update collab.
# Working directory: frontend
#
@ -120,10 +120,10 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c51
# To switch to the local path, run:
# scripts/tool/update_collab_source.sh
# ⚠️⚠️⚠️️
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2d7b1838e463ce0348cf700ff43f33f5718203be" }
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2d7b1838e463ce0348cf700ff43f33f5718203be" }
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2d7b1838e463ce0348cf700ff43f33f5718203be" }
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2d7b1838e463ce0348cf700ff43f33f5718203be" }
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2d7b1838e463ce0348cf700ff43f33f5718203be" }
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2d7b1838e463ce0348cf700ff43f33f5718203be" }
collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2d7b1838e463ce0348cf700ff43f33f5718203be" }
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" }
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" }
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" }
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" }
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" }
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" }
collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" }

View File

@ -36,6 +36,7 @@ impl EventIntegrationTest {
meta: Default::default(),
set_as_current: true,
index: None,
section: None,
};
EventBuilder::new(self.clone())
.event(FolderEvent::CreateView)
@ -66,6 +67,7 @@ impl EventIntegrationTest {
meta: Default::default(),
set_as_current: true,
index: None,
section: None,
};
EventBuilder::new(self.clone())
.event(FolderEvent::CreateView)
@ -91,6 +93,7 @@ impl EventIntegrationTest {
meta: Default::default(),
set_as_current: true,
index: None,
section: None,
};
EventBuilder::new(self.clone())
.event(FolderEvent::CreateView)

View File

@ -64,6 +64,7 @@ impl DocumentEventTest {
meta: Default::default(),
set_as_current: true,
index: None,
section: None,
};
EventBuilder::new(core.clone())
.event(FolderEvent::CreateView)

View File

@ -41,6 +41,7 @@ impl EventIntegrationTest {
meta: Default::default(),
set_as_current: true,
index: None,
section: None,
};
let view = EventBuilder::new(self.clone())
.event(FolderEvent::CreateView)

View File

@ -57,7 +57,7 @@ impl EventIntegrationTest {
pub async fn get_all_workspace_views(&self) -> Vec<ViewPB> {
EventBuilder::new(self.clone())
.event(FolderEvent::ReadWorkspaceViews)
.event(FolderEvent::ReadCurrentWorkspaceViews)
.async_send()
.await
.parse::<RepeatedViewPB>()
@ -115,6 +115,7 @@ impl EventIntegrationTest {
meta: Default::default(),
set_as_current: false,
index: None,
section: None,
};
EventBuilder::new(self.clone())
.event(FolderEvent::CreateView)
@ -165,6 +166,7 @@ impl ViewTest {
meta: Default::default(),
set_as_current: true,
index: None,
section: None,
};
let view = EventBuilder::new(sdk.clone())

View File

@ -276,9 +276,9 @@ impl EventIntegrationTest {
.parse()
}
pub async fn folder_read_workspace_views(&self) -> RepeatedViewPB {
pub async fn folder_read_current_workspace_views(&self) -> RepeatedViewPB {
EventBuilder::new(self.clone())
.event(FolderEvent::ReadWorkspaceViews)
.event(FolderEvent::ReadCurrentWorkspaceViews)
.async_send()
.await
.parse()

View File

@ -246,6 +246,7 @@ pub async fn create_view(
meta: Default::default(),
set_as_current: true,
index: None,
section: None,
};
EventBuilder::new(sdk.clone())
.event(CreateView)
@ -275,6 +276,8 @@ pub async fn move_view(
view_id,
new_parent_id: parent_id,
prev_view_id,
from_section: None,
to_section: None,
};
let error = EventBuilder::new(sdk.clone())
.event(MoveNestedView)

View File

@ -549,6 +549,8 @@ async fn move_folder_nested_view(
view_id,
new_parent_id,
prev_view_id,
from_section: None,
to_section: None,
};
EventBuilder::new(sdk)
.event(flowy_folder::event_map::FolderEvent::MoveNestedView)

View File

@ -77,7 +77,7 @@ async fn af_cloud_create_workspace_test() {
// before opening new workspace
let folder_ws = test.folder_read_current_workspace().await;
assert_eq!(&folder_ws.id, first_workspace_id);
let views = test.folder_read_workspace_views().await;
let views = test.folder_read_current_workspace_views().await;
assert_eq!(views.items[0].parent_view_id.as_str(), first_workspace_id);
}
{
@ -85,7 +85,7 @@ async fn af_cloud_create_workspace_test() {
test.open_workspace(&created_workspace.workspace_id).await;
let folder_ws = test.folder_read_current_workspace().await;
assert_eq!(folder_ws.id, created_workspace.workspace_id);
let views = test.folder_read_workspace_views().await;
let views = test.folder_read_current_workspace_views().await;
assert_eq!(
views.items[0].parent_view_id.as_str(),
created_workspace.workspace_id

View File

@ -59,6 +59,7 @@ impl ViewBuilder {
layout: ViewLayout::Document,
child_views: vec![],
is_favorite: false,
icon: None,
}
}

View File

@ -118,6 +118,15 @@ impl std::convert::From<ViewLayout> for ViewLayoutPB {
}
}
#[derive(Eq, PartialEq, Debug, Default, ProtoBuf, Clone)]
pub struct SectionViewsPB {
#[pb(index = 1)]
pub section: ViewSectionPB,
#[pb(index = 2)]
pub views: Vec<ViewPB>,
}
#[derive(Eq, PartialEq, Debug, Default, ProtoBuf, Clone)]
pub struct RepeatedViewPB {
#[pb(index = 1)]
@ -181,6 +190,20 @@ pub struct CreateViewPayloadPB {
// If the index is None or the index is out of range, the view will be appended to the end of the parent view.
#[pb(index = 9, one_of)]
pub index: Option<u32>,
// The section of the view.
// Only the view in public section will be shown in the shared workspace view list.
// The view in private section will only be shown in the user's private view list.
#[pb(index = 10, one_of)]
pub section: Option<ViewSectionPB>,
}
#[derive(Eq, PartialEq, Hash, Debug, ProtoBuf_Enum, Clone, Default)]
pub enum ViewSectionPB {
#[default]
// only support public and private section now.
Private = 0,
Public = 1,
}
/// The orphan view is meant to be a view that is not attached to any parent view. By default, this
@ -218,6 +241,8 @@ pub struct CreateViewParams {
// The index of the view in the parent view.
// If the index is None or the index is out of range, the view will be appended to the end of the parent view.
pub index: Option<u32>,
// The section of the view.
pub section: Option<ViewSectionPB>,
}
impl TryInto<CreateViewParams> for CreateViewPayloadPB {
@ -238,6 +263,7 @@ impl TryInto<CreateViewParams> for CreateViewPayloadPB {
meta: self.meta,
set_as_current: self.set_as_current,
index: self.index,
section: self.section,
})
}
}
@ -259,6 +285,8 @@ impl TryInto<CreateViewParams> for CreateOrphanViewPayloadPB {
meta: Default::default(),
set_as_current: false,
index: None,
// TODO: lucas.xu add section to CreateOrphanViewPayloadPB
section: Some(ViewSectionPB::Public),
})
}
}
@ -384,6 +412,12 @@ pub struct MoveNestedViewPayloadPB {
#[pb(index = 3, one_of)]
pub prev_view_id: Option<String>,
#[pb(index = 4, one_of)]
pub from_section: Option<ViewSectionPB>,
#[pb(index = 5, one_of)]
pub to_section: Option<ViewSectionPB>,
}
pub struct MoveViewParams {
@ -405,10 +439,13 @@ impl TryInto<MoveViewParams> for MoveViewPayloadPB {
}
}
#[derive(Debug)]
pub struct MoveNestedViewParams {
pub view_id: String,
pub new_parent_id: String,
pub prev_view_id: Option<String>,
pub from_section: Option<ViewSectionPB>,
pub to_section: Option<ViewSectionPB>,
}
impl TryInto<MoveNestedViewParams> for MoveNestedViewPayloadPB {
@ -422,6 +459,8 @@ impl TryInto<MoveNestedViewParams> for MoveNestedViewPayloadPB {
view_id,
new_parent_id,
prev_view_id,
from_section: self.from_section,
to_section: self.to_section,
})
}
}

View File

@ -97,6 +97,42 @@ pub struct WorkspaceIdPB {
pub value: String,
}
#[derive(Clone, Debug)]
pub struct WorkspaceIdParams {
pub value: String,
}
impl TryInto<WorkspaceIdParams> for WorkspaceIdPB {
type Error = ErrorCode;
fn try_into(self) -> Result<WorkspaceIdParams, Self::Error> {
Ok(WorkspaceIdParams {
value: WorkspaceIdentify::parse(self.value)?.0,
})
}
}
#[derive(Clone, ProtoBuf, Default, Debug)]
pub struct GetWorkspaceViewPB {
#[pb(index = 1)]
pub value: String,
}
#[derive(Clone, Debug)]
pub struct GetWorkspaceViewParams {
pub value: String,
}
impl TryInto<GetWorkspaceViewParams> for GetWorkspaceViewPB {
type Error = ErrorCode;
fn try_into(self) -> Result<GetWorkspaceViewParams, Self::Error> {
Ok(GetWorkspaceViewParams {
value: WorkspaceIdentify::parse(self.value)?.0,
})
}
}
#[derive(Default, ProtoBuf, Debug, Clone)]
pub struct WorkspaceSettingPB {
#[pb(index = 1)]

View File

@ -48,6 +48,18 @@ pub(crate) async fn get_all_workspace_handler(
#[tracing::instrument(level = "debug", skip(folder), err)]
pub(crate) async fn get_workspace_views_handler(
data: AFPluginData<GetWorkspaceViewPB>,
folder: AFPluginState<Weak<FolderManager>>,
) -> DataResult<RepeatedViewPB, FlowyError> {
let folder = upgrade_folder(folder)?;
let params: GetWorkspaceViewParams = data.into_inner().try_into()?;
let child_views = folder.get_workspace_views(&params.value).await?;
let repeated_view: RepeatedViewPB = child_views.into();
data_result_ok(repeated_view)
}
#[tracing::instrument(level = "debug", skip(folder), err)]
pub(crate) async fn get_current_workspace_views_handler(
folder: AFPluginState<Weak<FolderManager>>,
) -> DataResult<RepeatedViewPB, FlowyError> {
let folder = upgrade_folder(folder)?;
@ -56,6 +68,18 @@ pub(crate) async fn get_workspace_views_handler(
data_result_ok(repeated_view)
}
#[tracing::instrument(level = "debug", skip(folder), err)]
pub(crate) async fn read_private_views_handler(
data: AFPluginData<GetWorkspaceViewPB>,
folder: AFPluginState<Weak<FolderManager>>,
) -> DataResult<RepeatedViewPB, FlowyError> {
let folder = upgrade_folder(folder)?;
let params: GetWorkspaceViewParams = data.into_inner().try_into()?;
let child_views = folder.get_workspace_private_views(&params.value).await?;
let repeated_view: RepeatedViewPB = child_views.into();
data_result_ok(repeated_view)
}
#[tracing::instrument(level = "debug", skip(folder), err)]
pub(crate) async fn read_current_workspace_setting_handler(
folder: AFPluginState<Weak<FolderManager>>,
@ -212,9 +236,7 @@ pub(crate) async fn move_nested_view_handler(
) -> Result<(), FlowyError> {
let folder = upgrade_folder(folder)?;
let params: MoveNestedViewParams = data.into_inner().try_into()?;
folder
.move_nested_view(params.view_id, params.new_parent_id, params.prev_view_id)
.await?;
folder.move_nested_view(params).await?;
Ok(())
}

View File

@ -38,6 +38,8 @@ pub fn init(folder: Weak<FolderManager>) -> AFPlugin {
.event(FolderEvent::ToggleFavorite, toggle_favorites_handler)
.event(FolderEvent::UpdateRecentViews, update_recent_views_handler)
.event(FolderEvent::ReloadWorkspace, reload_workspace_handler)
.event(FolderEvent::ReadPrivateViews, read_private_views_handler)
.event(FolderEvent::ReadCurrentWorkspaceViews, get_current_workspace_views_handler)
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)]
@ -59,9 +61,9 @@ pub enum FolderEvent {
#[event(input = "WorkspaceIdPB")]
DeleteWorkspace = 3,
/// Return a list of views of the current workspace.
/// Return a list of views of the specified workspace.
/// Only the first level of child views are included.
#[event(input = "WorkspaceIdPB", output = "RepeatedViewPB")]
#[event(input = "GetWorkspaceViewPB", output = "RepeatedViewPB")]
ReadWorkspaceViews = 5,
/// Create a new view in the corresponding app
@ -156,4 +158,12 @@ pub enum FolderEvent {
#[event()]
ReloadWorkspace = 38,
#[event(input = "GetWorkspaceViewPB", output = "RepeatedViewPB")]
ReadPrivateViews = 39,
/// Return a list of views of the current workspace.
/// Only the first level of child views are included.
#[event(output = "RepeatedViewPB")]
ReadCurrentWorkspaceViews = 40,
}

View File

@ -23,8 +23,8 @@ use lib_infra::conditional_send_sync_trait;
use crate::entities::icon::UpdateViewIconParams;
use crate::entities::{
view_pb_with_child_views, view_pb_without_child_views, CreateViewParams, CreateWorkspaceParams,
DeletedViewPB, FolderSnapshotPB, RepeatedTrashPB, RepeatedViewIdPB, RepeatedViewPB,
UpdateViewParams, ViewPB, WorkspacePB, WorkspaceSettingPB,
DeletedViewPB, FolderSnapshotPB, MoveNestedViewParams, RepeatedTrashPB, RepeatedViewIdPB,
RepeatedViewPB, UpdateViewParams, ViewPB, ViewSectionPB, WorkspacePB, WorkspaceSettingPB,
};
use crate::manager_observer::{
notify_child_views_changed, notify_did_update_workspace, notify_parent_view_did_change,
@ -113,7 +113,7 @@ impl FolderManager {
},
|folder| {
let workspace_pb_from_workspace = |workspace: Workspace, folder: &Folder| {
let views = get_workspace_view_pbs(&workspace.id, folder);
let views = get_workspace_public_view_pbs(&workspace.id, folder);
let workspace: WorkspacePB = (workspace, views).into();
Ok::<WorkspacePB, FlowyError>(workspace)
};
@ -145,7 +145,15 @@ impl FolderManager {
pub async fn get_workspace_views(&self, workspace_id: &str) -> FlowyResult<Vec<ViewPB>> {
let views = self.with_folder(Vec::new, |folder| {
get_workspace_view_pbs(workspace_id, folder)
get_workspace_public_view_pbs(workspace_id, folder)
});
Ok(views)
}
pub async fn get_workspace_private_views(&self, workspace_id: &str) -> FlowyResult<Vec<ViewPB>> {
let views = self.with_folder(Vec::new, |folder| {
get_workspace_private_view_pbs(workspace_id, folder)
});
Ok(views)
@ -452,11 +460,16 @@ impl FolderManager {
}
let index = params.index;
let section = params.section.clone().unwrap_or(ViewSectionPB::Public);
let is_private = section == ViewSectionPB::Private;
let view = create_view(self.user.user_id()?, params, view_layout);
self.with_folder(
|| (),
|folder| {
folder.insert_view(view.clone(), index);
if is_private {
folder.add_private_view_ids(vec![view.id.clone()]);
}
},
);
@ -609,18 +622,26 @@ impl FolderManager {
/// * `prev_view_id` - An `Option<String>` that holds the id of the view after which the `view_id` should be positioned.
///
#[tracing::instrument(level = "trace", skip(self), err)]
pub async fn move_nested_view(
&self,
view_id: String,
new_parent_id: String,
prev_view_id: Option<String>,
) -> FlowyResult<()> {
pub async fn move_nested_view(&self, params: MoveNestedViewParams) -> FlowyResult<()> {
let view_id = params.view_id;
let new_parent_id = params.new_parent_id;
let prev_view_id = params.prev_view_id;
let from_section = params.from_section;
let to_section = params.to_section;
let view = self.get_view_pb(&view_id).await?;
let old_parent_id = view.parent_view_id;
self.with_folder(
|| (),
|folder| {
folder.move_nested_view(&view_id, &new_parent_id, prev_view_id);
if from_section != to_section {
if to_section == Some(ViewSectionPB::Private) {
folder.add_private_view_ids(vec![view_id.clone()]);
} else {
folder.delete_private_view_ids(vec![view_id.clone()]);
}
}
},
);
notify_parent_view_did_change(
@ -743,6 +764,8 @@ impl FolderManager {
meta: Default::default(),
set_as_current: true,
index,
// TODO: lucas.xu fetch the section from the view
section: Some(ViewSectionPB::Public),
};
self.create_view_with_params(duplicate_params).await?;
@ -954,6 +977,8 @@ impl FolderManager {
meta: Default::default(),
set_as_current: false,
index: None,
// TODO: Lucas.xu fetch the section from the view
section: Some(ViewSectionPB::Public),
};
let view = create_view(self.user.user_id()?, params, import_data.view_layout);
@ -1110,16 +1135,61 @@ impl FolderManager {
}
}
/// Return the views that belong to the workspace. The views are filtered by the trash.
pub(crate) fn get_workspace_view_pbs(_workspace_id: &str, folder: &Folder) -> Vec<ViewPB> {
let items = folder.get_all_trash();
let trash_ids = items
/// Return the views that belong to the workspace. The views are filtered by the trash and all the private views.
pub(crate) fn get_workspace_public_view_pbs(_workspace_id: &str, folder: &Folder) -> Vec<ViewPB> {
// get the trash ids
let trash_ids = folder
.get_all_trash()
.into_iter()
.map(|trash| trash.id)
.collect::<Vec<String>>();
// get the private view ids
let private_view_ids = folder
.get_all_private_views()
.into_iter()
.map(|view| view.id)
.collect::<Vec<String>>();
let mut views = folder.get_workspace_views();
views.retain(|view| !trash_ids.contains(&view.id));
// filter the views that are in the trash and all the private views
views.retain(|view| !trash_ids.contains(&view.id) && !private_view_ids.contains(&view.id));
views
.into_iter()
.map(|view| {
// Get child views
let child_views = folder
.views
.get_views_belong_to(&view.id)
.into_iter()
.collect();
view_pb_with_child_views(view, child_views)
})
.collect()
}
/// Get the current private views of the user.
pub(crate) fn get_workspace_private_view_pbs(_workspace_id: &str, folder: &Folder) -> Vec<ViewPB> {
// get the trash ids
let trash_ids = folder
.get_all_trash()
.into_iter()
.map(|trash| trash.id)
.collect::<Vec<String>>();
// get the private view ids
let private_view_ids = folder
.get_my_private_views()
.into_iter()
.map(|view| view.id)
.collect::<Vec<String>>();
let mut views = folder.get_workspace_views();
// filter the views that are in the trash and not in the private view ids
views.retain(|view| !trash_ids.contains(&view.id) && private_view_ids.contains(&view.id));
views
.into_iter()

View File

@ -14,9 +14,9 @@ use lib_dispatch::prelude::af_spawn;
use crate::entities::{
view_pb_with_child_views, view_pb_without_child_views, ChildViewUpdatePB, FolderSnapshotStatePB,
FolderSyncStatePB, RepeatedTrashPB, RepeatedViewPB, ViewPB,
FolderSyncStatePB, RepeatedTrashPB, RepeatedViewPB, SectionViewsPB, ViewPB, ViewSectionPB,
};
use crate::manager::{get_workspace_view_pbs, MutexFolder};
use crate::manager::{get_workspace_private_view_pbs, get_workspace_public_view_pbs, MutexFolder};
use crate::notification::{send_notification, FolderNotification};
/// Listen on the [ViewChange] after create/delete/update events happened
@ -161,7 +161,8 @@ pub(crate) fn notify_parent_view_did_change<T: AsRef<str>>(
// if the view's parent id equal to workspace id. Then it will fetch the current
// workspace views. Because the the workspace is not a view stored in the views map.
if parent_view_id == workspace_id {
notify_did_update_workspace(&workspace_id, folder)
notify_did_update_workspace(&workspace_id, folder);
notify_did_update_section_views(&workspace_id, folder);
} else {
// Parent view can contain a list of child views. Currently, only get the first level
// child views.
@ -181,8 +182,35 @@ pub(crate) fn notify_parent_view_did_change<T: AsRef<str>>(
None
}
pub(crate) fn notify_did_update_section_views(workspace_id: &str, folder: &Folder) {
let public_views = get_workspace_public_view_pbs(workspace_id, folder);
let private_views = get_workspace_private_view_pbs(workspace_id, folder);
tracing::trace!(
"Did update section views: public len = {}, private len = {}",
public_views.len(),
private_views.len()
);
// TODO(Lucas.xu) - Only notify the section changed, not the public/private both.
// Notify the public views
send_notification(workspace_id, FolderNotification::DidUpdateSectionViews)
.payload(SectionViewsPB {
section: ViewSectionPB::Public,
views: public_views,
})
.send();
// Notify the private views
send_notification(workspace_id, FolderNotification::DidUpdateSectionViews)
.payload(SectionViewsPB {
section: ViewSectionPB::Private,
views: private_views,
})
.send();
}
pub(crate) fn notify_did_update_workspace(workspace_id: &str, folder: &Folder) {
let repeated_view: RepeatedViewPB = get_workspace_view_pbs(workspace_id, folder).into();
let repeated_view: RepeatedViewPB = get_workspace_public_view_pbs(workspace_id, folder).into();
tracing::trace!("Did update workspace views: {:?}", repeated_view);
send_notification(workspace_id, FolderNotification::DidUpdateWorkspaceViews)
.payload(repeated_view)

View File

@ -35,6 +35,9 @@ pub enum FolderNotification {
DidUnfavoriteView = 37,
DidUpdateRecentViews = 38,
/// Trigger when the ROOT views (the first level) in section are updated
DidUpdateSectionViews = 39,
}
impl std::convert::From<FolderNotification> for i32 {
@ -60,6 +63,8 @@ impl std::convert::From<i32> for FolderNotification {
17 => FolderNotification::DidUpdateFolderSyncUpdate,
36 => FolderNotification::DidFavoriteView,
37 => FolderNotification::DidUnfavoriteView,
38 => FolderNotification::DidUpdateRecentViews,
39 => FolderNotification::DidUpdateSectionViews,
_ => FolderNotification::Unknown,
}
}

View File

@ -2,7 +2,7 @@ use std::collections::HashMap;
use flowy_folder_pub::cloud::gen_view_id;
use crate::entities::{CreateViewParams, ViewLayoutPB};
use crate::entities::{CreateViewParams, ViewLayoutPB, ViewSectionPB};
use crate::manager::FolderManager;
#[cfg(feature = "test_helper")]
@ -47,6 +47,7 @@ impl FolderManager {
meta: ext,
set_as_current: true,
index: None,
section: Some(ViewSectionPB::Public),
};
self.create_view_with_params(params).await.unwrap();
view_id

View File

@ -54,6 +54,7 @@ impl DefaultFolderBuilder {
favorites: Default::default(),
recent: Default::default(),
trash: Default::default(),
private: Default::default(),
}
}
}