Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
canifa_note
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Vũ Hoàng Anh
canifa_note
Commits
3fd305dc
Unverified
Commit
3fd305dc
authored
Aug 11, 2025
by
varsnotwars
Committed by
GitHub
Aug 11, 2025
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix: preferences being overwritten (#4990)
parent
c76ffb0f
Changes
2
Show whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
231 additions
and
1 deletion
+231
-1
user_service.go
server/router/api/v1/user_service.go
+85
-1
user_service_test.go
server/router/api/v1/user_service_test.go
+146
-0
No files found.
server/router/api/v1/user_service.go
View file @
3fd305dc
...
@@ -373,8 +373,23 @@ func (s *APIV1Service) UpdateUserSetting(ctx context.Context, request *v1pb.Upda
...
@@ -373,8 +373,23 @@ func (s *APIV1Service) UpdateUserSetting(ctx context.Context, request *v1pb.Upda
return
nil
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"invalid setting key: %v"
,
err
)
return
nil
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"invalid setting key: %v"
,
err
)
}
}
// get existing user setting
existingUserSetting
,
err
:=
s
.
Store
.
GetUserSetting
(
ctx
,
&
store
.
FindUserSetting
{
UserID
:
&
userID
,
Key
:
storeKey
,
})
if
err
!=
nil
{
return
nil
,
err
}
if
existingUserSetting
==
nil
{
return
nil
,
status
.
Errorf
(
codes
.
NotFound
,
"%s not found"
,
storeKey
.
String
())
}
// merge only the fields specified by UpdateMask
merged
:=
mergeUserSettingWithMask
(
existingUserSetting
,
request
.
Setting
,
storeKey
,
request
.
UpdateMask
.
Paths
)
// Convert API setting to store setting
// Convert API setting to store setting
storeSetting
,
err
:=
convertUserSettingToStore
(
request
.
Setting
,
userID
,
storeKey
)
storeSetting
,
err
:=
convertUserSettingToStore
(
merged
,
userID
,
storeKey
)
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"failed to convert setting: %v"
,
err
)
return
nil
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"failed to convert setting: %v"
,
err
)
}
}
...
@@ -1320,3 +1335,72 @@ func (s *APIV1Service) validateUserFilter(_ context.Context, filterStr string) e
...
@@ -1320,3 +1335,72 @@ func (s *APIV1Service) validateUserFilter(_ context.Context, filterStr string) e
}
}
return
nil
return
nil
}
}
func
mergeUserSettingWithMask
(
existing
*
storepb
.
UserSetting
,
incoming
*
v1pb
.
UserSetting
,
key
storepb
.
UserSetting_Key
,
paths
[]
string
)
*
v1pb
.
UserSetting
{
if
incoming
==
nil
{
return
&
v1pb
.
UserSetting
{}
}
switch
key
{
case
storepb
.
UserSetting_GENERAL
:
var
gs
*
v1pb
.
UserSetting_GeneralSetting
if
existing
==
nil
{
gs
=
&
v1pb
.
UserSetting_GeneralSetting
{
Locale
:
"en"
,
Appearance
:
"system"
,
MemoVisibility
:
"PRIVATE"
,
Theme
:
""
,
}
}
else
{
gs
=
&
v1pb
.
UserSetting_GeneralSetting
{
Appearance
:
existing
.
GetGeneral
()
.
GetAppearance
(),
MemoVisibility
:
existing
.
GetGeneral
()
.
GetMemoVisibility
(),
Locale
:
existing
.
GetGeneral
()
.
GetLocale
(),
Theme
:
existing
.
GetGeneral
()
.
GetTheme
(),
}
}
for
_
,
field
:=
range
paths
{
switch
field
{
case
"appearance"
:
gs
.
Appearance
=
incoming
.
GetGeneralSetting
()
.
Appearance
case
"memoVisibility"
:
gs
.
MemoVisibility
=
incoming
.
GetGeneralSetting
()
.
MemoVisibility
case
"theme"
:
gs
.
Theme
=
incoming
.
GetGeneralSetting
()
.
Theme
case
"locale"
:
gs
.
Locale
=
incoming
.
GetGeneralSetting
()
.
Locale
}
}
return
&
v1pb
.
UserSetting
{
Name
:
incoming
.
Name
,
Value
:
&
v1pb
.
UserSetting_GeneralSetting_
{
GeneralSetting
:
gs
,
},
}
case
storepb
.
UserSetting_SHORTCUTS
:
// handled by the FE calling shortcut_service.CreateShortcut
// if the FE wants to modify shortcuts by calling the user_service we need to handle below
return
incoming
case
storepb
.
UserSetting_WEBHOOKS
:
// handled by the FE calling user_service.CreateUserWebhook
// if the FE wants to modify webhooks by calling the user_service we need to handle below
return
incoming
case
storepb
.
UserSetting_ACCESS_TOKENS
:
// handled by the FE calling user_service.CreateUserAccessToken
// if the FE wants to modify access tokens by calling the user_service we need to handle below
return
incoming
case
storepb
.
UserSetting_SESSIONS
:
// handled by the FE calling auth_service.CreateSession
// if the FE wants to modify sessions by calling the user_service we need to handle below
return
incoming
default
:
return
incoming
}
}
server/router/api/v1/user_service_test.go
0 → 100644
View file @
3fd305dc
package
v1
import
(
"reflect"
"testing"
v1pb
"github.com/usememos/memos/proto/gen/api/v1"
storepb
"github.com/usememos/memos/proto/gen/store"
)
func
TestMergeUserSettingWithMask
(
t
*
testing
.
T
)
{
tests
:=
[]
struct
{
name
string
existing
*
storepb
.
UserSetting
incoming
*
v1pb
.
UserSetting
key
storepb
.
UserSetting_Key
paths
[]
string
expected
*
v1pb
.
UserSetting
}{
{
name
:
"adds new field without removing existing fields"
,
existing
:
&
storepb
.
UserSetting
{
UserId
:
1
,
Key
:
storepb
.
UserSetting_GENERAL
,
Value
:
&
storepb
.
UserSetting_General
{
General
:
&
storepb
.
GeneralUserSetting
{
MemoVisibility
:
"PROTECTED"
,
},
},
},
incoming
:
&
v1pb
.
UserSetting
{
Value
:
&
v1pb
.
UserSetting_GeneralSetting_
{
GeneralSetting
:
&
v1pb
.
UserSetting_GeneralSetting
{
Appearance
:
"light"
,
},
},
},
key
:
storepb
.
UserSetting_GENERAL
,
paths
:
[]
string
{
"appearance"
},
expected
:
&
v1pb
.
UserSetting
{
Value
:
&
v1pb
.
UserSetting_GeneralSetting_
{
GeneralSetting
:
&
v1pb
.
UserSetting_GeneralSetting
{
Appearance
:
"light"
,
MemoVisibility
:
"PROTECTED"
,
},
},
},
},
{
name
:
"adds new field when no existing fields exist"
,
existing
:
&
storepb
.
UserSetting
{
UserId
:
1
,
Key
:
storepb
.
UserSetting_GENERAL
,
Value
:
&
storepb
.
UserSetting_General
{},
},
incoming
:
&
v1pb
.
UserSetting
{
Value
:
&
v1pb
.
UserSetting_GeneralSetting_
{
GeneralSetting
:
&
v1pb
.
UserSetting_GeneralSetting
{
Theme
:
"whitewall"
,
},
},
},
key
:
storepb
.
UserSetting_GENERAL
,
paths
:
[]
string
{
"theme"
},
expected
:
&
v1pb
.
UserSetting
{
Value
:
&
v1pb
.
UserSetting_GeneralSetting_
{
GeneralSetting
:
&
v1pb
.
UserSetting_GeneralSetting
{
Theme
:
"whitewall"
,
},
},
},
},
{
name
:
"updates existing field without removing existing fields"
,
existing
:
&
storepb
.
UserSetting
{
UserId
:
1
,
Key
:
storepb
.
UserSetting_GENERAL
,
Value
:
&
storepb
.
UserSetting_General
{
General
:
&
storepb
.
GeneralUserSetting
{
Appearance
:
"dark"
,
MemoVisibility
:
"PUBLIC"
,
},
},
},
incoming
:
&
v1pb
.
UserSetting
{
Value
:
&
v1pb
.
UserSetting_GeneralSetting_
{
GeneralSetting
:
&
v1pb
.
UserSetting_GeneralSetting
{
Appearance
:
"light"
,
},
},
},
key
:
storepb
.
UserSetting_GENERAL
,
paths
:
[]
string
{
"appearance"
},
expected
:
&
v1pb
.
UserSetting
{
Value
:
&
v1pb
.
UserSetting_GeneralSetting_
{
GeneralSetting
:
&
v1pb
.
UserSetting_GeneralSetting
{
Appearance
:
"light"
,
MemoVisibility
:
"PUBLIC"
,
},
},
},
},
{
name
:
"updates multiple fields without removing existing fields"
,
existing
:
&
storepb
.
UserSetting
{
UserId
:
1
,
Key
:
storepb
.
UserSetting_GENERAL
,
Value
:
&
storepb
.
UserSetting_General
{
General
:
&
storepb
.
GeneralUserSetting
{
Appearance
:
"system"
,
},
},
},
incoming
:
&
v1pb
.
UserSetting
{
Value
:
&
v1pb
.
UserSetting_GeneralSetting_
{
GeneralSetting
:
&
v1pb
.
UserSetting_GeneralSetting
{
Appearance
:
"dark"
,
Theme
:
"paper"
,
MemoVisibility
:
"PROTECTED"
,
},
},
},
key
:
storepb
.
UserSetting_GENERAL
,
paths
:
[]
string
{
"theme"
,
"memoVisibility"
,
"appearance"
},
expected
:
&
v1pb
.
UserSetting
{
Value
:
&
v1pb
.
UserSetting_GeneralSetting_
{
GeneralSetting
:
&
v1pb
.
UserSetting_GeneralSetting
{
Appearance
:
"dark"
,
MemoVisibility
:
"PROTECTED"
,
Theme
:
"paper"
,
},
},
},
},
}
for
_
,
tt
:=
range
tests
{
t
.
Run
(
tt
.
name
,
func
(
t
*
testing
.
T
)
{
actual
:=
mergeUserSettingWithMask
(
tt
.
existing
,
tt
.
incoming
,
tt
.
key
,
tt
.
paths
)
if
!
reflect
.
DeepEqual
(
actual
,
tt
.
expected
)
{
t
.
Errorf
(
"expected %v but got %v"
,
tt
.
expected
,
actual
)
}
})
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment