feat: use new api to get the workspace member role (#6783)

* chore: update to latest client api

* chore: merge with main

* chore: remove unneeded code

* fix: sqlite migration

* fix: cargo fmt

* feat: use new api to get the member role

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
This commit is contained in:
Zack 2024-12-19 15:19:24 +08:00 committed by GitHub
parent dda3962249
commit ee96a44fef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 114 additions and 88 deletions

View File

@ -15,8 +15,11 @@ part 'local_ai_on_boarding_bloc.freezed.dart';
class LocalAIOnBoardingBloc
extends Bloc<LocalAIOnBoardingEvent, LocalAIOnBoardingState> {
LocalAIOnBoardingBloc(this.userProfile, this.member, this.workspaceId)
: super(const LocalAIOnBoardingState()) {
LocalAIOnBoardingBloc(
this.userProfile,
this.currentWorkspaceMemberRole,
this.workspaceId,
) : super(const LocalAIOnBoardingState()) {
_userService = UserBackendService(userId: userProfile.id);
_successListenable = getIt<SubscriptionSuccessListenable>();
_successListenable.addListener(_onPaymentSuccessful);
@ -36,7 +39,7 @@ class LocalAIOnBoardingBloc
}
final UserProfilePB userProfile;
final WorkspaceMemberPB member;
final AFRolePB? currentWorkspaceMemberRole;
final String workspaceId;
late final IUserBackendService _userService;
late final SubscriptionSuccessListenable _successListenable;

View File

@ -14,13 +14,18 @@ class SettingsAIBloc extends Bloc<SettingsAIEvent, SettingsAIState> {
SettingsAIBloc(
this.userProfile,
this.workspaceId,
WorkspaceMemberPB? member,
AFRolePB? currentWorkspaceMemberRole,
) : _userListener = UserListener(userProfile: userProfile),
_userService = UserBackendService(userId: userProfile.id),
super(SettingsAIState(userProfile: userProfile, member: member)) {
super(
SettingsAIState(
userProfile: userProfile,
currentWorkspaceMemberRole: currentWorkspaceMemberRole,
),
) {
_dispatch();
if (member == null) {
if (currentWorkspaceMemberRole == null) {
_userService.getWorkspaceMember().then((result) {
result.fold(
(member) {
@ -85,7 +90,7 @@ class SettingsAIBloc extends Bloc<SettingsAIEvent, SettingsAIState> {
);
},
refreshMember: (member) {
emit(state.copyWith(member: member));
emit(state.copyWith(currentWorkspaceMemberRole: member.role));
},
);
});
@ -152,7 +157,7 @@ class SettingsAIState with _$SettingsAIState {
const factory SettingsAIState({
required UserProfilePB userProfile,
UseAISettingPB? aiSettings,
WorkspaceMemberPB? member,
AFRolePB? currentWorkspaceMemberRole,
@Default(true) bool enableSearchIndexing,
}) = _SettingsAIState;
}

View File

@ -33,7 +33,7 @@ class SettingsDialogBloc
extends Bloc<SettingsDialogEvent, SettingsDialogState> {
SettingsDialogBloc(
this.userProfile,
this.workspaceMember, {
this.currentWorkspaceMemberRole, {
SettingsPage? initPage,
}) : _userListener = UserListener(userProfile: userProfile),
super(SettingsDialogState.initial(userProfile, initPage)) {
@ -41,7 +41,7 @@ class SettingsDialogBloc
}
final UserProfilePB userProfile;
final WorkspaceMemberPB? workspaceMember;
final AFRolePB? currentWorkspaceMemberRole;
final UserListener _userListener;
@override
@ -57,8 +57,10 @@ class SettingsDialogBloc
initial: () async {
_userListener.start(onProfileUpdated: _profileUpdated);
final isBillingEnabled =
await _isBillingEnabled(userProfile, workspaceMember);
final isBillingEnabled = await _isBillingEnabled(
userProfile,
currentWorkspaceMemberRole,
);
if (isBillingEnabled) {
emit(state.copyWith(isBillingEnabled: true));
}
@ -86,7 +88,7 @@ class SettingsDialogBloc
Future<bool> _isBillingEnabled(
UserProfilePB userProfile, [
WorkspaceMemberPB? member,
AFRolePB? currentWorkspaceMemberRole,
]) async {
if ([
AuthenticatorPB.Local,
@ -94,7 +96,8 @@ class SettingsDialogBloc
return false;
}
if (member == null || member.role != AFRolePB.Owner) {
if (currentWorkspaceMemberRole == null ||
currentWorkspaceMemberRole != AFRolePB.Owner) {
return false;
}

View File

@ -63,13 +63,6 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
actionResult: null,
),
);
/// We wait with fetching the workspace member as it may take some time,
/// to avoid blocking the UI from rendering (the sidebar).
final workspaceMemberResult =
await _userService.getWorkspaceMember();
final workspaceMember = workspaceMemberResult.toNullable();
emit(state.copyWith(currentWorkspaceMember: workspaceMember));
},
fetchWorkspaces: () async {
final result = await _fetchWorkspaces();
@ -238,14 +231,6 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
),
),
);
/// We wait with fetching the workspace member as it may take some time,
/// to avoid blocking the UI from rendering (the sidebar).
final workspaceMemberResult =
await _userService.getWorkspaceMember();
final workspaceMember = workspaceMemberResult.toNullable();
emit(state.copyWith(currentWorkspaceMember: workspaceMember));
},
renameWorkspace: (workspaceId, name) async {
final result =
@ -515,7 +500,6 @@ class UserWorkspaceState with _$UserWorkspaceState {
const factory UserWorkspaceState({
@Default(null) UserWorkspacePB? currentWorkspace,
@Default([]) List<UserWorkspacePB> workspaces,
@Default(null) WorkspaceMemberPB? currentWorkspaceMember,
@Default(null) UserWorkspaceActionResult? actionResult,
@Default(false) bool isCollabWorkspaceOn,
}) = _UserWorkspaceState;
@ -533,7 +517,6 @@ class UserWorkspaceState with _$UserWorkspaceState {
if (identical(this, other)) return true;
return other is UserWorkspaceState &&
other.currentWorkspaceMember == currentWorkspaceMember &&
other.currentWorkspace == currentWorkspace &&
_deepCollectionEquality.equals(other.workspaces, workspaces) &&
identical(other.actionResult, actionResult);

View File

@ -94,15 +94,15 @@ class SidebarToast extends StatelessWidget {
}
final userWorkspaceBloc = context.read<UserWorkspaceBloc>();
final member = userWorkspaceBloc.state.currentWorkspaceMember;
if (member == null) {
final role = userWorkspaceBloc.state.currentWorkspace?.role;
if (role == null) {
return Log.error(
"Member is null. It should not happen. If you see this error, it's a bug",
);
}
// Only if the user is the workspace owner will we navigate to the plan page.
if (member.role.isOwner) {
if (role.isOwner) {
showSettingsDialog(
context,
userProfile,

View File

@ -150,7 +150,7 @@ class SpaceMoreActionTypeWrapper extends CustomActionCell {
final isOwner = context
.read<UserWorkspaceBloc?>()
?.state
.currentWorkspaceMember
.currentWorkspace
?.role
.isOwner ??
false;

View File

@ -1,21 +1,20 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/shared/af_role_pb_extension.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/workspace/application/settings/ai/local_ai_on_boarding_bloc.dart';
import 'package:appflowy/workspace/application/settings/ai/settings_ai_bloc.dart';
import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart';
import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/settings/ai/settings_ai_bloc.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class AIFeatureOnlySupportedWhenUsingAppFlowyCloud extends StatelessWidget {
@ -39,19 +38,20 @@ class SettingsAIView extends StatelessWidget {
const SettingsAIView({
super.key,
required this.userProfile,
required this.member,
required this.currentWorkspaceMemberRole,
required this.workspaceId,
});
final UserProfilePB userProfile;
final WorkspaceMemberPB? member;
final AFRolePB? currentWorkspaceMemberRole;
final String workspaceId;
@override
Widget build(BuildContext context) {
return BlocProvider<SettingsAIBloc>(
create: (_) => SettingsAIBloc(userProfile, workspaceId, member)
..add(const SettingsAIEvent.started()),
create: (_) =>
SettingsAIBloc(userProfile, workspaceId, currentWorkspaceMemberRole)
..add(const SettingsAIEvent.started()),
child: BlocBuilder<SettingsAIBloc, SettingsAIState>(
builder: (context, state) {
final children = <Widget>[
@ -60,11 +60,11 @@ class SettingsAIView extends StatelessWidget {
children.add(const _AISearchToggle(value: false));
if (state.member != null) {
if (state.currentWorkspaceMemberRole != null) {
children.add(
_LocalAIOnBoarding(
userProfile: userProfile,
member: state.member!,
currentWorkspaceMemberRole: state.currentWorkspaceMemberRole!,
workspaceId: workspaceId,
),
);
@ -129,11 +129,11 @@ class _AISearchToggle extends StatelessWidget {
class _LocalAIOnBoarding extends StatelessWidget {
const _LocalAIOnBoarding({
required this.userProfile,
required this.member,
required this.currentWorkspaceMemberRole,
required this.workspaceId,
});
final UserProfilePB userProfile;
final WorkspaceMemberPB member;
final AFRolePB? currentWorkspaceMemberRole;
final String workspaceId;
@override
@ -142,16 +142,18 @@ class _LocalAIOnBoarding extends StatelessWidget {
return BillingGateGuard(
builder: (context) {
return BlocProvider(
create: (context) =>
LocalAIOnBoardingBloc(userProfile, member, workspaceId)
..add(const LocalAIOnBoardingEvent.started()),
create: (context) => LocalAIOnBoardingBloc(
userProfile,
currentWorkspaceMemberRole,
workspaceId,
)..add(const LocalAIOnBoardingEvent.started()),
child: BlocBuilder<LocalAIOnBoardingBloc, LocalAIOnBoardingState>(
builder: (context, state) {
// Show the local AI settings if the user has purchased the AI Local plan
if (kDebugMode || state.isPurchaseAILocal) {
return const LocalAISetting();
} else {
if (member.role.isOwner) {
if (currentWorkspaceMemberRole?.isOwner ?? false) {
// Show the upgrade to AI Local plan button if the user has not purchased the AI Local plan
return _UpgradeToAILocalPlan(
onTap: () {

View File

@ -1,7 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
@ -45,6 +43,7 @@ import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:google_fonts/google_fonts.dart';
@ -52,11 +51,11 @@ class SettingsWorkspaceView extends StatelessWidget {
const SettingsWorkspaceView({
super.key,
required this.userProfile,
this.workspaceMember,
this.currentWorkspaceMemberRole,
});
final UserProfilePB userProfile;
final WorkspaceMemberPB? workspaceMember;
final AFRolePB? currentWorkspaceMemberRole;
@override
Widget build(BuildContext context) {
@ -93,7 +92,11 @@ class SettingsWorkspaceView extends StatelessWidget {
SettingsCategory(
title: LocaleKeys.settings_workspacePage_workspaceName_title
.tr(),
children: [_WorkspaceNameSetting(member: workspaceMember)],
children: [
_WorkspaceNameSetting(
currentWorkspaceMemberRole: currentWorkspaceMemberRole,
),
],
),
const SettingsCategorySpacer(),
SettingsCategory(
@ -104,7 +107,7 @@ class SettingsWorkspaceView extends StatelessWidget {
.tr(),
children: [
_WorkspaceIconSetting(
enableEdit: workspaceMember?.role.isOwner ?? false,
enableEdit: currentWorkspaceMemberRole?.isOwner ?? false,
workspace: state.workspace,
),
],
@ -185,14 +188,14 @@ class SettingsWorkspaceView extends StatelessWidget {
fontWeight: FontWeight.w600,
onPressed: () => showConfirmDialog(
context: context,
title: workspaceMember?.role.isOwner ?? false
title: currentWorkspaceMemberRole?.isOwner ?? false
? LocaleKeys
.settings_workspacePage_deleteWorkspacePrompt_title
.tr()
: LocaleKeys
.settings_workspacePage_leaveWorkspacePrompt_title
.tr(),
description: workspaceMember?.role.isOwner ?? false
description: currentWorkspaceMemberRole?.isOwner ?? false
? LocaleKeys
.settings_workspacePage_deleteWorkspacePrompt_content
.tr()
@ -201,13 +204,13 @@ class SettingsWorkspaceView extends StatelessWidget {
.tr(),
style: ConfirmPopupStyle.cancelAndOk,
onConfirm: () => context.read<WorkspaceSettingsBloc>().add(
workspaceMember?.role.isOwner ?? false
currentWorkspaceMemberRole?.isOwner ?? false
? const WorkspaceSettingsEvent.deleteWorkspace()
: const WorkspaceSettingsEvent.leaveWorkspace(),
),
),
buttonType: SingleSettingsButtonType.danger,
buttonLabel: workspaceMember?.role.isOwner ?? false
buttonLabel: currentWorkspaceMemberRole?.isOwner ?? false
? LocaleKeys
.settings_workspacePage_manageWorkspace_deleteWorkspace
.tr()
@ -225,9 +228,11 @@ class SettingsWorkspaceView extends StatelessWidget {
}
class _WorkspaceNameSetting extends StatefulWidget {
const _WorkspaceNameSetting({this.member});
const _WorkspaceNameSetting({
this.currentWorkspaceMemberRole,
});
final WorkspaceMemberPB? member;
final AFRolePB? currentWorkspaceMemberRole;
@override
State<_WorkspaceNameSetting> createState() => _WorkspaceNameSettingState();
@ -255,7 +260,8 @@ class _WorkspaceNameSettingState extends State<_WorkspaceNameSetting> {
}
},
builder: (_, state) {
if (widget.member == null || !widget.member!.role.isOwner) {
if (widget.currentWorkspaceMemberRole == null ||
!widget.currentWorkspaceMemberRole!.isOwner) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2.5),
child: FlowyText.regular(

View File

@ -109,7 +109,7 @@ class _HomePageButton extends StatelessWidget {
final isOwner = context
.watch<UserWorkspaceBloc>()
.state
.currentWorkspaceMember
.currentWorkspace
?.role
.isOwner ??
false;
@ -227,7 +227,7 @@ class _FreePlanUpgradeButton extends StatelessWidget {
final isOwner = context
.watch<UserWorkspaceBloc>()
.state
.currentWorkspaceMember
.currentWorkspace
?.role
.isOwner ??
false;

View File

@ -82,7 +82,7 @@ class _DomainMoreActionState extends State<DomainMoreAction> {
final isOwner = context
.watch<UserWorkspaceBloc>()
.state
.currentWorkspaceMember
.currentWorkspace
?.role
.isOwner ??
false;

View File

@ -57,7 +57,7 @@ class SettingsDialog extends StatelessWidget {
return BlocProvider<SettingsDialogBloc>(
create: (context) => SettingsDialogBloc(
user,
context.read<UserWorkspaceBloc>().state.currentWorkspaceMember,
context.read<UserWorkspaceBloc>().state.currentWorkspace?.role,
initPage: initPage,
)..add(const SettingsDialogEvent.initial()),
child: BlocBuilder<SettingsDialogBloc, SettingsDialogState>(
@ -80,10 +80,6 @@ class SettingsDialog extends StatelessWidget {
currentPage:
context.read<SettingsDialogBloc>().state.page,
isBillingEnabled: state.isBillingEnabled,
member: context
.read<UserWorkspaceBloc>()
.state
.currentWorkspaceMember,
),
),
Expanded(
@ -98,7 +94,8 @@ class SettingsDialog extends StatelessWidget {
context
.read<UserWorkspaceBloc>()
.state
.currentWorkspaceMember,
.currentWorkspace
?.role,
),
),
],
@ -114,7 +111,7 @@ class SettingsDialog extends StatelessWidget {
String workspaceId,
SettingsPage page,
UserProfilePB user,
WorkspaceMemberPB? member,
AFRolePB? currentWorkspaceMemberRole,
) {
switch (page) {
case SettingsPage.account:
@ -126,7 +123,7 @@ class SettingsDialog extends StatelessWidget {
case SettingsPage.workspace:
return SettingsWorkspaceView(
userProfile: user,
workspaceMember: member,
currentWorkspaceMemberRole: currentWorkspaceMemberRole,
);
case SettingsPage.manageData:
return SettingsManageDataView(userProfile: user);
@ -140,7 +137,7 @@ class SettingsDialog extends StatelessWidget {
if (user.authenticator == AuthenticatorPB.AppFlowyCloud) {
return SettingsAIView(
userProfile: user,
member: member,
currentWorkspaceMemberRole: currentWorkspaceMemberRole,
workspaceId: workspaceId,
);
} else {

View File

@ -16,14 +16,12 @@ class SettingsMenu extends StatelessWidget {
required this.currentPage,
required this.userProfile,
required this.isBillingEnabled,
this.member,
});
final Function changeSelectedPage;
final SettingsPage currentPage;
final UserProfilePB userProfile;
final bool isBillingEnabled;
final WorkspaceMemberPB? member;
@override
Widget build(BuildContext context) {

View File

@ -11,4 +11,4 @@ flowy-error = { workspace = true }
client-api = { workspace = true }
bytes.workspace = true
futures.workspace = true
serde_json.workspace = true
serde_json.workspace = true

View File

@ -667,6 +667,7 @@ fn to_user_workspace(af_workspace: AFWorkspace) -> UserWorkspace {
workspace_database_id: af_workspace.database_storage_id.to_string(),
icon: af_workspace.icon,
member_count: af_workspace.member_count.unwrap_or(0),
role: af_workspace.role.map(|r| r.into()),
}
}

View File

@ -34,7 +34,7 @@ impl UserCloudService for LocalServerUserAuthServiceImpl {
let params = params.unbox_or_error::<SignUpParams>()?;
let uid = ID_GEN.lock().await.next_id();
let workspace_id = uuid::Uuid::new_v4().to_string();
let user_workspace = UserWorkspace::new(&workspace_id, uid);
let user_workspace = UserWorkspace::new_local(&workspace_id, uid);
let user_name = if params.name.is_empty() {
DEFAULT_USER_NAME()
} else {
@ -214,6 +214,7 @@ fn make_user_workspace() -> UserWorkspace {
created_at: Default::default(),
workspace_database_id: uuid::Uuid::new_v4().to_string(),
icon: "".to_string(),
member_count: 0,
member_count: 1,
role: None,
}
}

View File

@ -0,0 +1 @@
ALTER TABLE user_workspace_table DROP COLUMN role;

View File

@ -0,0 +1 @@
ALTER TABLE user_workspace_table ADD COLUMN role INT;

View File

@ -102,6 +102,7 @@ diesel::table! {
database_storage_id -> Text,
icon -> Text,
member_count -> BigInt,
role -> Nullable<Integer>,
}
}

View File

@ -2,6 +2,7 @@ use std::str::FromStr;
use chrono::{DateTime, Utc};
pub use client_api::entity::billing_dto::RecurringInterval;
use client_api::entity::AFRole;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use serde_repr::*;
@ -145,10 +146,12 @@ pub struct UserWorkspace {
pub icon: String,
#[serde(default)]
pub member_count: i64,
#[serde(default)]
pub role: Option<Role>,
}
impl UserWorkspace {
pub fn new(workspace_id: &str, _uid: i64) -> Self {
pub fn new_local(workspace_id: &str, _uid: i64) -> Self {
Self {
id: workspace_id.to_string(),
name: "".to_string(),
@ -156,6 +159,7 @@ impl UserWorkspace {
workspace_database_id: Uuid::new_v4().to_string(),
icon: "".to_string(),
member_count: 1,
role: None,
}
}
}
@ -420,6 +424,16 @@ impl From<Role> for i32 {
}
}
impl From<AFRole> for Role {
fn from(value: AFRole) -> Self {
match value {
AFRole::Owner => Role::Owner,
AFRole::Member => Role::Member,
AFRole::Guest => Role::Guest,
}
}
}
pub struct WorkspaceMember {
pub email: String,
pub role: Role,

View File

@ -75,7 +75,8 @@ impl<'de> Visitor<'de> for SessionVisitor {
// For historical reasons, the database_storage_id is constructed by the user_id.
workspace_database_id: STANDARD.encode(format!("{}:user:database", user_id)),
icon: "".to_owned(),
member_count: 0,
member_count: 1,
role: None,
})
}
}

View File

@ -11,6 +11,7 @@ use crate::entities::{AIModelPB, AuthenticatorPB};
use crate::errors::ErrorCode;
use super::parser::UserStabilityAIKey;
use super::AFRolePB;
#[derive(Default, ProtoBuf)]
pub struct UserTokenPB {
@ -237,6 +238,9 @@ pub struct UserWorkspacePB {
#[pb(index = 5)]
pub member_count: i64,
#[pb(index = 6, one_of)]
pub role: Option<AFRolePB>,
}
impl From<UserWorkspace> for UserWorkspacePB {
@ -247,6 +251,7 @@ impl From<UserWorkspace> for UserWorkspacePB {
created_at_timestamp: value.created_at.timestamp(),
icon: value.icon,
member_count: value.member_count,
role: value.role.map(AFRolePB::from),
}
}
}

View File

@ -147,7 +147,7 @@ pub struct UpdateWorkspaceMemberPB {
}
// Workspace Role
#[derive(ProtoBuf_Enum, Clone, Default)]
#[derive(Debug, ProtoBuf_Enum, Clone, Default)]
pub enum AFRolePB {
Owner = 0,
Member = 1,

View File

@ -17,6 +17,7 @@ pub struct UserWorkspaceTable {
pub database_storage_id: String,
pub icon: String,
pub member_count: i64,
pub role: Option<i32>,
}
pub fn get_user_workspace_op(workspace_id: &str, mut conn: DBConnection) -> Option<UserWorkspace> {
@ -94,6 +95,7 @@ impl TryFrom<(i64, &UserWorkspace)> for UserWorkspaceTable {
database_storage_id: value.1.workspace_database_id.clone(),
icon: value.1.icon.clone(),
member_count: value.1.member_count,
role: value.1.role.clone().map(|v| v as i32),
})
}
}
@ -110,6 +112,7 @@ impl From<UserWorkspaceTable> for UserWorkspace {
workspace_database_id: value.database_storage_id,
icon: value.icon,
member_count: value.member_count,
role: value.role.map(|v| v.into()),
}
}
}

View File

@ -730,6 +730,7 @@ pub fn save_all_user_workspaces(
user_workspace_table::database_storage_id.eq(&user_workspace.database_storage_id),
user_workspace_table::icon.eq(&user_workspace.icon),
user_workspace_table::member_count.eq(&user_workspace.member_count),
user_workspace_table::role.eq(&user_workspace.role),
))
.execute(conn)?;