mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: support for switching workspace on mobile (#4990)
* feat: support switching workspaces on mobile * fix: sync recent section
This commit is contained in:
parent
b8e3de97a5
commit
a1b183f330
@ -5,7 +5,6 @@ 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/application/user/user_workspace_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.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';
|
||||
@ -18,12 +17,12 @@ class MobileFolders extends StatelessWidget {
|
||||
const MobileFolders({
|
||||
super.key,
|
||||
required this.user,
|
||||
required this.workspaceSetting,
|
||||
required this.workspaceId,
|
||||
required this.showFavorite,
|
||||
});
|
||||
|
||||
final UserProfilePB user;
|
||||
final WorkspaceSettingPB workspaceSetting;
|
||||
final String workspaceId;
|
||||
final bool showFavorite;
|
||||
|
||||
@override
|
||||
@ -35,7 +34,7 @@ class MobileFolders extends StatelessWidget {
|
||||
..add(
|
||||
SidebarSectionsEvent.initial(
|
||||
user,
|
||||
workspaceSetting.workspaceId,
|
||||
workspaceId,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -43,47 +42,57 @@ class MobileFolders extends StatelessWidget {
|
||||
create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()),
|
||||
),
|
||||
],
|
||||
child: BlocConsumer<SidebarSectionsBloc, SidebarSectionsState>(
|
||||
listenWhen: (p, c) =>
|
||||
p.lastCreatedRootView?.id != c.lastCreatedRootView?.id,
|
||||
child: BlocListener<UserWorkspaceBloc, UserWorkspaceState>(
|
||||
listener: (context, state) {
|
||||
final lastCreatedRootView = state.lastCreatedRootView;
|
||||
if (lastCreatedRootView != null) {
|
||||
context.pushView(lastCreatedRootView);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
final isCollaborativeWorkspace =
|
||||
context.read<UserWorkspaceBloc>().state.isCollabWorkspaceOn;
|
||||
return SlidableAutoCloseBehavior(
|
||||
child: Column(
|
||||
children: [
|
||||
...isCollaborativeWorkspace
|
||||
? [
|
||||
MobileSectionFolder(
|
||||
title: LocaleKeys.sideBar_public.tr(),
|
||||
categoryType: FolderCategoryType.public,
|
||||
views: state.section.publicViews,
|
||||
),
|
||||
const VSpace(8.0),
|
||||
MobileSectionFolder(
|
||||
title: LocaleKeys.sideBar_private.tr(),
|
||||
categoryType: FolderCategoryType.private,
|
||||
views: state.section.privateViews,
|
||||
),
|
||||
]
|
||||
: [
|
||||
MobileSectionFolder(
|
||||
title: LocaleKeys.sideBar_personal.tr(),
|
||||
categoryType: FolderCategoryType.public,
|
||||
views: state.section.publicViews,
|
||||
),
|
||||
],
|
||||
const VSpace(8.0),
|
||||
],
|
||||
),
|
||||
);
|
||||
context.read<SidebarSectionsBloc>().add(
|
||||
SidebarSectionsEvent.initial(
|
||||
user,
|
||||
state.currentWorkspace?.workspaceId ?? workspaceId,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: BlocConsumer<SidebarSectionsBloc, SidebarSectionsState>(
|
||||
listenWhen: (p, c) =>
|
||||
p.lastCreatedRootView?.id != c.lastCreatedRootView?.id,
|
||||
listener: (context, state) {
|
||||
final lastCreatedRootView = state.lastCreatedRootView;
|
||||
if (lastCreatedRootView != null) {
|
||||
context.pushView(lastCreatedRootView);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
final isCollaborativeWorkspace =
|
||||
context.read<UserWorkspaceBloc>().state.isCollabWorkspaceOn;
|
||||
return SlidableAutoCloseBehavior(
|
||||
child: Column(
|
||||
children: [
|
||||
...isCollaborativeWorkspace
|
||||
? [
|
||||
MobileSectionFolder(
|
||||
title: LocaleKeys.sideBar_public.tr(),
|
||||
categoryType: FolderCategoryType.public,
|
||||
views: state.section.publicViews,
|
||||
),
|
||||
const VSpace(8.0),
|
||||
MobileSectionFolder(
|
||||
title: LocaleKeys.sideBar_private.tr(),
|
||||
categoryType: FolderCategoryType.private,
|
||||
views: state.section.privateViews,
|
||||
),
|
||||
]
|
||||
: [
|
||||
MobileSectionFolder(
|
||||
title: LocaleKeys.sideBar_personal.tr(),
|
||||
categoryType: FolderCategoryType.public,
|
||||
views: state.section.publicViews,
|
||||
),
|
||||
],
|
||||
const VSpace(8.0),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -127,7 +127,9 @@ class MobileHomePage extends StatelessWidget {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: MobileFolders(
|
||||
user: userProfile,
|
||||
workspaceSetting: workspaceSetting,
|
||||
workspaceId:
|
||||
state.currentWorkspace?.workspaceId ??
|
||||
workspaceSetting.workspaceId,
|
||||
showFavorite: false,
|
||||
),
|
||||
),
|
||||
|
@ -1,6 +1,8 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/mobile_home_setting_page.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart';
|
||||
import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart';
|
||||
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
@ -43,8 +45,9 @@ class MobileHomePageHeader extends StatelessWidget {
|
||||
: _MobileUser(userProfile: userProfile),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () =>
|
||||
context.push(MobileHomeSettingPage.routeName),
|
||||
onPressed: () => context.push(
|
||||
MobileHomeSettingPage.routeName,
|
||||
),
|
||||
icon: const FlowySvg(FlowySvgs.m_setting_m),
|
||||
),
|
||||
],
|
||||
@ -108,25 +111,88 @@ class _MobileWorkspace extends StatelessWidget {
|
||||
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,
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
_showSwitchWorkspacesBottomSheet(
|
||||
context,
|
||||
currentWorkspace,
|
||||
workspaces,
|
||||
);
|
||||
},
|
||||
child: 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,
|
||||
const HSpace(8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
FlowyText.medium(
|
||||
currentWorkspace.name,
|
||||
fontSize: 16.0,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const HSpace(4.0),
|
||||
const FlowySvg(FlowySvgs.list_dropdown_s),
|
||||
],
|
||||
),
|
||||
FlowyText.medium(
|
||||
userProfile.email.isNotEmpty
|
||||
? userProfile.email
|
||||
: userProfile.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showSwitchWorkspacesBottomSheet(
|
||||
BuildContext context,
|
||||
UserWorkspacePB currentWorkspace,
|
||||
List<UserWorkspacePB> workspaces,
|
||||
) {
|
||||
showMobileBottomSheet(
|
||||
context,
|
||||
showDivider: false,
|
||||
showHeader: true,
|
||||
showDragHandle: true,
|
||||
title: LocaleKeys.workspace_menuTitle.tr(),
|
||||
builder: (_) {
|
||||
return MobileWorkspaceMenu(
|
||||
userProfile: userProfile,
|
||||
currentWorkspace: currentWorkspace,
|
||||
workspaces: workspaces,
|
||||
onWorkspaceSelected: (workspace) {
|
||||
context.pop();
|
||||
|
||||
if (workspace == currentWorkspace) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.read<UserWorkspaceBloc>().add(
|
||||
UserWorkspaceEvent.openWorkspace(
|
||||
workspace.workspaceId,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/recent_folder/mobile_recent_view.dart';
|
||||
import 'package:appflowy/workspace/application/recent/prelude.dart';
|
||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
@ -22,31 +23,38 @@ class _MobileRecentFolderState extends State<MobileRecentFolder> {
|
||||
..add(
|
||||
const RecentViewsEvent.initial(),
|
||||
),
|
||||
child: BlocBuilder<RecentViewsBloc, RecentViewsState>(
|
||||
builder: (context, state) {
|
||||
final ids = <String>{};
|
||||
|
||||
List<ViewPB> recentViews = state.views.reversed.toList();
|
||||
recentViews.retainWhere((element) => ids.add(element.id));
|
||||
|
||||
// only keep the first 20 items.
|
||||
recentViews = recentViews.take(20).toList();
|
||||
|
||||
if (recentViews.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
_RecentViews(
|
||||
key: ValueKey(recentViews),
|
||||
// the recent views are in reverse order
|
||||
recentViews: recentViews,
|
||||
),
|
||||
const VSpace(12.0),
|
||||
],
|
||||
);
|
||||
child: BlocListener<UserWorkspaceBloc, UserWorkspaceState>(
|
||||
listener: (context, state) {
|
||||
context.read<RecentViewsBloc>().add(
|
||||
const RecentViewsEvent.fetchRecentViews(),
|
||||
);
|
||||
},
|
||||
child: BlocBuilder<RecentViewsBloc, RecentViewsState>(
|
||||
builder: (context, state) {
|
||||
final ids = <String>{};
|
||||
|
||||
List<ViewPB> recentViews = state.views.reversed.toList();
|
||||
recentViews.retainWhere((element) => ids.add(element.id));
|
||||
|
||||
// only keep the first 20 items.
|
||||
recentViews = recentViews.take(20).toList();
|
||||
|
||||
if (recentViews.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
_RecentViews(
|
||||
key: ValueKey(recentViews),
|
||||
// the recent views are in reverse order
|
||||
recentViews: recentViews,
|
||||
),
|
||||
const VSpace(12.0),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,50 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
// Only works on mobile.
|
||||
class MobileWorkspaceMenu extends StatelessWidget {
|
||||
const MobileWorkspaceMenu({
|
||||
super.key,
|
||||
required this.userProfile,
|
||||
required this.currentWorkspace,
|
||||
required this.workspaces,
|
||||
required this.onWorkspaceSelected,
|
||||
});
|
||||
|
||||
final UserProfilePB userProfile;
|
||||
final UserWorkspacePB currentWorkspace;
|
||||
final List<UserWorkspacePB> workspaces;
|
||||
final void Function(UserWorkspacePB workspace) onWorkspaceSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<Widget> children = [];
|
||||
for (var i = 0; i < workspaces.length; i++) {
|
||||
final workspace = workspaces[i];
|
||||
children.add(
|
||||
FlowyOptionTile.text(
|
||||
text: workspace.name,
|
||||
showTopBorder: i == 0,
|
||||
leftIcon: WorkspaceIcon(
|
||||
enableEdit: false,
|
||||
iconSize: 22,
|
||||
workspace: workspace,
|
||||
),
|
||||
trailing: workspace.workspaceId == currentWorkspace.workspaceId
|
||||
? const FlowySvg(
|
||||
FlowySvgs.m_blue_check_s,
|
||||
blendMode: null,
|
||||
)
|
||||
: null,
|
||||
onTap: () => onWorkspaceSelected(workspace),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Column(
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
}
|
@ -1,10 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/core/helpers/url_launcher.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/startup/tasks/device_info_task.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/feature_flags/mobile_feature_flag_screen.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../widgets/widgets.dart';
|
||||
|
||||
@ -32,6 +34,16 @@ class AboutSettingGroup extends StatelessWidget {
|
||||
),
|
||||
onTap: () => afLaunchUrlString('https://appflowy.io/terms/app'),
|
||||
),
|
||||
if (kDebugMode)
|
||||
MobileSettingItem(
|
||||
name: 'Feature Flags',
|
||||
trailing: const Icon(
|
||||
Icons.chevron_right,
|
||||
),
|
||||
onTap: () {
|
||||
context.push(FeatureFlagScreen.routeName);
|
||||
},
|
||||
),
|
||||
MobileSettingItem(
|
||||
name: LocaleKeys.settings_mobile_version.tr(),
|
||||
trailing: FlowyText(
|
||||
|
@ -100,7 +100,7 @@ enum FeatureFlag {
|
||||
case FeatureFlag.membersSettings:
|
||||
return 'if it\'s on, you can see the members settings in the settings page';
|
||||
case FeatureFlag.syncDocument:
|
||||
return 'if it\'s on, the document will be synced the events from server in real-time';
|
||||
return 'if it\'s on, the document will be synced in real-time';
|
||||
case FeatureFlag.unknown:
|
||||
return '';
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ import 'package:appflowy/startup/tasks/app_widget.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/user/presentation/presentation.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/desktop_home_screen.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/feature_flags/mobile_feature_flag_screen.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flowy_infra/time/duration.dart';
|
||||
@ -53,6 +54,7 @@ GoRouter generateRouter(Widget child) {
|
||||
_mobileHomeSettingPageRoute(),
|
||||
_mobileCloudSettingAppFlowyCloudPageRoute(),
|
||||
_mobileLaunchSettingsPageRoute(),
|
||||
_mobileFeatureFlagPageRoute(),
|
||||
|
||||
// view page
|
||||
_mobileEditorScreenRoute(),
|
||||
@ -219,6 +221,16 @@ GoRoute _mobileLaunchSettingsPageRoute() {
|
||||
);
|
||||
}
|
||||
|
||||
GoRoute _mobileFeatureFlagPageRoute() {
|
||||
return GoRoute(
|
||||
parentNavigatorKey: AppGlobals.rootNavKey,
|
||||
path: FeatureFlagScreen.routeName,
|
||||
pageBuilder: (context, state) {
|
||||
return const MaterialExtendedPage(child: FeatureFlagScreen());
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
GoRoute _mobileHomeTrashPageRoute() {
|
||||
return GoRoute(
|
||||
parentNavigatorKey: AppGlobals.rootNavKey,
|
||||
|
@ -28,14 +28,19 @@ class _WorkspaceIconState extends State<WorkspaceIcon> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final child = widget.workspace.icon.isNotEmpty
|
||||
? FlowyText(
|
||||
widget.workspace.icon,
|
||||
textAlign: TextAlign.center,
|
||||
fontSize: widget.iconSize,
|
||||
Widget child = widget.workspace.icon.isNotEmpty
|
||||
? Container(
|
||||
width: widget.iconSize,
|
||||
margin: const EdgeInsets.all(2),
|
||||
child: FlowyText(
|
||||
widget.workspace.icon,
|
||||
textAlign: TextAlign.center,
|
||||
fontSize: widget.iconSize,
|
||||
),
|
||||
)
|
||||
: Container(
|
||||
alignment: Alignment.center,
|
||||
width: widget.iconSize,
|
||||
decoration: BoxDecoration(
|
||||
color: ColorGenerator.generateColorFromString(
|
||||
widget.workspace.name,
|
||||
@ -51,29 +56,32 @@ class _WorkspaceIconState extends State<WorkspaceIcon> {
|
||||
color: Colors.black,
|
||||
),
|
||||
);
|
||||
return AppFlowyPopover(
|
||||
offset: const Offset(0, 8),
|
||||
controller: controller,
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
constraints: BoxConstraints.loose(const Size(360, 380)),
|
||||
clickHandler: PopoverClickHandler.gestureDetector,
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
return FlowyIconPicker(
|
||||
onSelected: (result) {
|
||||
context.read<UserWorkspaceBloc>().add(
|
||||
UserWorkspaceEvent.updateWorkspaceIcon(
|
||||
widget.workspace.workspaceId,
|
||||
result.emoji,
|
||||
),
|
||||
);
|
||||
controller.close();
|
||||
},
|
||||
);
|
||||
},
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
if (widget.enableEdit) {
|
||||
child = AppFlowyPopover(
|
||||
offset: const Offset(0, 8),
|
||||
controller: controller,
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
constraints: BoxConstraints.loose(const Size(360, 380)),
|
||||
clickHandler: PopoverClickHandler.gestureDetector,
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
return FlowyIconPicker(
|
||||
onSelected: (result) {
|
||||
context.read<UserWorkspaceBloc>().add(
|
||||
UserWorkspaceEvent.updateWorkspaceIcon(
|
||||
widget.workspace.workspaceId,
|
||||
result.emoji,
|
||||
),
|
||||
);
|
||||
controller.close();
|
||||
},
|
||||
);
|
||||
},
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
@ -59,7 +59,7 @@ class _FeatureFlagItemState extends State<_FeatureFlagItem> {
|
||||
widget.featureFlag.description,
|
||||
maxLines: 3,
|
||||
),
|
||||
trailing: Switch(
|
||||
trailing: Switch.adaptive(
|
||||
value: widget.featureFlag.isOn,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
|
@ -0,0 +1,20 @@
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FeatureFlagScreen extends StatelessWidget {
|
||||
const FeatureFlagScreen({
|
||||
super.key,
|
||||
});
|
||||
|
||||
static const routeName = '/feature_flag';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Feature Flags'),
|
||||
),
|
||||
body: const FeatureFlagsPage(),
|
||||
);
|
||||
}
|
||||
}
|
@ -65,6 +65,7 @@
|
||||
"exportLogFiles": "Export log files",
|
||||
"reachOut": "Reach out on Discord"
|
||||
},
|
||||
"menuTitle": "Workspaces",
|
||||
"deleteWorkspaceHintText": "Are you sure you want to delete the workspace? This action cannot be undone.",
|
||||
"createSuccess": "Workspace created successfully",
|
||||
"createFailed": "Failed to create workspace",
|
||||
|
Loading…
Reference in New Issue
Block a user