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
ee96465b
Commit
ee96465b
authored
Jan 13, 2025
by
johnnyjoy
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: list user stats
parent
cde058c7
Changes
17
Hide whitespace changes
Inline
Side-by-side
Showing
17 changed files
with
266 additions
and
270 deletions
+266
-270
user_service.proto
proto/api/v1/user_service.proto
+3
-2
user_service.pb.go
proto/gen/api/v1/user_service.pb.go
+24
-14
apidocs.swagger.yaml
proto/gen/apidocs.swagger.yaml
+5
-2
acl_config.go
server/router/api/v1/acl_config.go
+1
-1
user_service.go
server/router/api/v1/user_service.go
+0
-94
user_service_stats.go
server/router/api/v1/user_service_stats.go
+124
-0
ExploreSidebar.tsx
web/src/components/ExploreSidebar/ExploreSidebar.tsx
+9
-8
HomeSidebar.tsx
web/src/components/HomeSidebar/HomeSidebar.tsx
+8
-9
TagsSection.tsx
web/src/components/HomeSidebar/TagsSection.tsx
+5
-7
TagSelector.tsx
web/src/components/MemoEditor/ActionButton/TagSelector.tsx
+2
-2
TagSuggestions.tsx
web/src/components/MemoEditor/Editor/TagSuggestions.tsx
+2
-2
PagedMemoList.tsx
web/src/components/PagedMemoList/PagedMemoList.tsx
+4
-10
RenameTagDialog.tsx
web/src/components/RenameTagDialog.tsx
+3
-3
StatisticsView.tsx
web/src/components/StatisticsView.tsx
+27
-39
index.ts
web/src/store/v1/index.ts
+1
-1
memoMetadata.ts
web/src/store/v1/memoMetadata.ts
+0
-76
userStats.ts
web/src/store/v1/userStats.ts
+48
-0
No files found.
proto/api/v1/user_service.proto
View file @
ee96465b
...
...
@@ -192,8 +192,9 @@ message UserStats {
message
MemoTypeStats
{
int32
link_count
=
1
;
int32
task_count
=
2
;
int32
code_count
=
3
;
int32
code_count
=
2
;
int32
todo_count
=
3
;
int32
undo_count
=
4
;
}
}
...
...
proto/gen/api/v1/user_service.pb.go
View file @
ee96465b
...
...
@@ -1286,8 +1286,9 @@ func (x *DeleteUserAccessTokenRequest) GetAccessToken() string {
type
UserStats_MemoTypeStats
struct
{
state
protoimpl
.
MessageState
`protogen:"open.v1"`
LinkCount
int32
`protobuf:"varint,1,opt,name=link_count,json=linkCount,proto3" json:"link_count,omitempty"`
TaskCount
int32
`protobuf:"varint,2,opt,name=task_count,json=taskCount,proto3" json:"task_count,omitempty"`
CodeCount
int32
`protobuf:"varint,3,opt,name=code_count,json=codeCount,proto3" json:"code_count,omitempty"`
CodeCount
int32
`protobuf:"varint,2,opt,name=code_count,json=codeCount,proto3" json:"code_count,omitempty"`
TodoCount
int32
`protobuf:"varint,3,opt,name=todo_count,json=todoCount,proto3" json:"todo_count,omitempty"`
UndoCount
int32
`protobuf:"varint,4,opt,name=undo_count,json=undoCount,proto3" json:"undo_count,omitempty"`
unknownFields
protoimpl
.
UnknownFields
sizeCache
protoimpl
.
SizeCache
}
...
...
@@ -1329,16 +1330,23 @@ func (x *UserStats_MemoTypeStats) GetLinkCount() int32 {
return
0
}
func
(
x
*
UserStats_MemoTypeStats
)
Get
Task
Count
()
int32
{
func
(
x
*
UserStats_MemoTypeStats
)
Get
Code
Count
()
int32
{
if
x
!=
nil
{
return
x
.
Task
Count
return
x
.
Code
Count
}
return
0
}
func
(
x
*
UserStats_MemoTypeStats
)
Get
Code
Count
()
int32
{
func
(
x
*
UserStats_MemoTypeStats
)
Get
Todo
Count
()
int32
{
if
x
!=
nil
{
return
x
.
CodeCount
return
x
.
TodoCount
}
return
0
}
func
(
x
*
UserStats_MemoTypeStats
)
GetUndoCount
()
int32
{
if
x
!=
nil
{
return
x
.
UndoCount
}
return
0
}
...
...
@@ -1428,7 +1436,7 @@ var file_api_v1_user_service_proto_rawDesc = []byte{
0x64
,
0x61
,
0x74
,
0x65
,
0x4d
,
0x61
,
0x73
,
0x6b
,
0x22
,
0x27
,
0x0a
,
0x11
,
0x44
,
0x65
,
0x6c
,
0x65
,
0x74
,
0x65
,
0x55
,
0x73
,
0x65
,
0x72
,
0x52
,
0x65
,
0x71
,
0x75
,
0x65
,
0x73
,
0x74
,
0x12
,
0x12
,
0x0a
,
0x04
,
0x6e
,
0x61
,
0x6d
,
0x65
,
0x18
,
0x01
,
0x20
,
0x01
,
0x28
,
0x09
,
0x52
,
0x04
,
0x6e
,
0x61
,
0x6d
,
0x65
,
0x22
,
0x
b
1
,
0x03
,
0x0a
,
0x09
,
0x55
,
0x73
,
0x65
,
0x72
,
0x53
,
0x74
,
0x61
,
0x74
,
0x73
,
0x12
,
0x65
,
0x22
,
0x
d
1
,
0x03
,
0x0a
,
0x09
,
0x55
,
0x73
,
0x65
,
0x72
,
0x53
,
0x74
,
0x61
,
0x74
,
0x73
,
0x12
,
0x12
,
0x0a
,
0x04
,
0x6e
,
0x61
,
0x6d
,
0x65
,
0x18
,
0x01
,
0x20
,
0x01
,
0x28
,
0x09
,
0x52
,
0x04
,
0x6e
,
0x61
,
0x6d
,
0x65
,
0x12
,
0x52
,
0x0a
,
0x17
,
0x6d
,
0x65
,
0x6d
,
0x6f
,
0x5f
,
0x64
,
0x69
,
0x73
,
0x70
,
0x6c
,
0x61
,
0x79
,
0x5f
,
0x74
,
0x69
,
0x6d
,
0x65
,
0x73
,
0x74
,
0x61
,
0x6d
,
0x70
,
0x73
,
0x18
,
0x02
,
...
...
@@ -1448,13 +1456,15 @@ var file_api_v1_user_service_proto_rawDesc = []byte{
0x67
,
0x43
,
0x6f
,
0x75
,
0x6e
,
0x74
,
0x45
,
0x6e
,
0x74
,
0x72
,
0x79
,
0x12
,
0x10
,
0x0a
,
0x03
,
0x6b
,
0x65
,
0x79
,
0x18
,
0x01
,
0x20
,
0x01
,
0x28
,
0x09
,
0x52
,
0x03
,
0x6b
,
0x65
,
0x79
,
0x12
,
0x14
,
0x0a
,
0x05
,
0x76
,
0x61
,
0x6c
,
0x75
,
0x65
,
0x18
,
0x02
,
0x20
,
0x01
,
0x28
,
0x05
,
0x52
,
0x05
,
0x76
,
0x61
,
0x6c
,
0x75
,
0x65
,
0x3a
,
0x02
,
0x38
,
0x01
,
0x1a
,
0x6c
,
0x0a
,
0x0d
,
0x4d
,
0x65
,
0x6d
,
0x6f
,
0x54
,
0x79
,
0x70
,
0x65
,
0x53
,
0x74
,
0x61
,
0x74
,
0x73
,
0x12
,
0x1d
,
0x0a
,
0x0a
,
0x6c
,
0x69
,
0x6e
,
0x6b
,
0x5f
,
0x63
,
0x6f
,
0x75
,
0x6e
,
0x74
,
0x18
,
0x01
,
0x20
,
0x01
,
0x28
,
0x05
,
0x52
,
0x09
,
0x6c
,
0x69
,
0x6e
,
0x6b
,
0x43
,
0x6f
,
0x75
,
0x6e
,
0x74
,
0x12
,
0x1d
,
0x0a
,
0x0a
,
0x74
,
0x61
,
0x73
,
0x6b
,
0x5f
,
0x63
,
0x6f
,
0x75
,
0x6e
,
0x74
,
0x18
,
0x02
,
0x20
,
0x01
,
0x28
,
0x05
,
0x52
,
0x09
,
0x74
,
0x61
,
0x73
,
0x6b
,
0x43
,
0x6f
,
0x75
,
0x6e
,
0x74
,
0x12
,
0x1d
,
0x0a
,
0x0a
,
0x63
,
0x6f
,
0x64
,
0x65
,
0x5f
,
0x63
,
0x6f
,
0x75
,
0x6e
,
0x74
,
0x18
,
0x03
,
0x20
,
0x01
,
0x28
,
0x05
,
0x52
,
0x09
,
0x63
,
0x6f
,
0x64
,
0x65
,
0x6c
,
0x75
,
0x65
,
0x3a
,
0x02
,
0x38
,
0x01
,
0x1a
,
0x8b
,
0x01
,
0x0a
,
0x0d
,
0x4d
,
0x65
,
0x6d
,
0x6f
,
0x54
,
0x79
,
0x70
,
0x65
,
0x53
,
0x74
,
0x61
,
0x74
,
0x73
,
0x12
,
0x1d
,
0x0a
,
0x0a
,
0x6c
,
0x69
,
0x6e
,
0x6b
,
0x5f
,
0x63
,
0x6f
,
0x75
,
0x6e
,
0x74
,
0x18
,
0x01
,
0x20
,
0x01
,
0x28
,
0x05
,
0x52
,
0x09
,
0x6c
,
0x69
,
0x6e
,
0x6b
,
0x43
,
0x6f
,
0x75
,
0x6e
,
0x74
,
0x12
,
0x1d
,
0x0a
,
0x0a
,
0x63
,
0x6f
,
0x64
,
0x65
,
0x5f
,
0x63
,
0x6f
,
0x75
,
0x6e
,
0x74
,
0x18
,
0x02
,
0x20
,
0x01
,
0x28
,
0x05
,
0x52
,
0x09
,
0x63
,
0x6f
,
0x64
,
0x65
,
0x43
,
0x6f
,
0x75
,
0x6e
,
0x74
,
0x12
,
0x1d
,
0x0a
,
0x0a
,
0x74
,
0x6f
,
0x64
,
0x6f
,
0x5f
,
0x63
,
0x6f
,
0x75
,
0x6e
,
0x74
,
0x18
,
0x03
,
0x20
,
0x01
,
0x28
,
0x05
,
0x52
,
0x09
,
0x74
,
0x6f
,
0x64
,
0x6f
,
0x43
,
0x6f
,
0x75
,
0x6e
,
0x74
,
0x12
,
0x1d
,
0x0a
,
0x0a
,
0x75
,
0x6e
,
0x64
,
0x6f
,
0x5f
,
0x63
,
0x6f
,
0x75
,
0x6e
,
0x74
,
0x18
,
0x04
,
0x20
,
0x01
,
0x28
,
0x05
,
0x52
,
0x09
,
0x75
,
0x6e
,
0x64
,
0x6f
,
0x43
,
0x6f
,
0x75
,
0x6e
,
0x74
,
0x22
,
0x31
,
0x0a
,
0x17
,
0x4c
,
0x69
,
0x73
,
0x74
,
0x41
,
0x6c
,
0x6c
,
0x55
,
0x73
,
0x65
,
0x72
,
0x53
,
0x74
,
0x61
,
0x74
,
0x73
,
0x52
,
0x65
,
0x71
,
0x75
,
0x65
,
0x73
,
0x74
,
0x12
,
0x16
,
0x0a
,
0x06
,
0x66
,
0x69
,
0x6c
,
0x74
,
0x65
,
0x72
,
0x18
,
0x01
,
0x20
,
0x01
,
0x28
,
0x09
,
...
...
proto/gen/apidocs.swagger.yaml
View file @
ee96465b
...
...
@@ -1868,10 +1868,13 @@ definitions:
linkCount
:
type
:
integer
format
:
int32
task
Count
:
code
Count
:
type
:
integer
format
:
int32
codeCount
:
todoCount
:
type
:
integer
format
:
int32
undoCount
:
type
:
integer
format
:
int32
WorkspaceStorageSettingS3Config
:
...
...
server/router/api/v1/acl_config.go
View file @
ee96465b
...
...
@@ -13,7 +13,7 @@ var authenticationAllowlistMethods = map[string]bool{
"/memos.api.v1.AuthService/SignUp"
:
true
,
"/memos.api.v1.UserService/GetUser"
:
true
,
"/memos.api.v1.UserService/GetUserAvatarBinary"
:
true
,
"/memos.api.v1.UserService/List
UserStats"
:
true
,
"/memos.api.v1.UserService/List
AllUserStats"
:
true
,
"/memos.api.v1.UserService/SearchUsers"
:
true
,
"/memos.api.v1.MemoService/GetMemo"
:
true
,
"/memos.api.v1.MemoService/GetMemoByUid"
:
true
,
...
...
server/router/api/v1/user_service.go
View file @
ee96465b
...
...
@@ -275,100 +275,6 @@ func (s *APIV1Service) DeleteUser(ctx context.Context, request *v1pb.DeleteUserR
return
&
emptypb
.
Empty
{},
nil
}
func
(
s
*
APIV1Service
)
ListAllUserStats
(
ctx
context
.
Context
,
request
*
v1pb
.
ListAllUserStatsRequest
)
(
*
v1pb
.
ListAllUserStatsResponse
,
error
)
{
users
,
err
:=
s
.
Store
.
ListUsers
(
ctx
,
&
store
.
FindUser
{})
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to list users: %v"
,
err
)
}
userStatsList
:=
[]
*
v1pb
.
UserStats
{}
for
_
,
user
:=
range
users
{
userStats
,
err
:=
s
.
GetUserStats
(
ctx
,
&
v1pb
.
GetUserStatsRequest
{
Name
:
fmt
.
Sprintf
(
"%s%d"
,
UserNamePrefix
,
user
.
ID
),
Filter
:
request
.
Filter
,
})
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to get user stats: %v"
,
err
)
}
userStatsList
=
append
(
userStatsList
,
userStats
)
}
return
&
v1pb
.
ListAllUserStatsResponse
{
UserStats
:
userStatsList
,
},
nil
}
func
(
s
*
APIV1Service
)
GetUserStats
(
ctx
context
.
Context
,
request
*
v1pb
.
GetUserStatsRequest
)
(
*
v1pb
.
UserStats
,
error
)
{
userID
,
err
:=
ExtractUserIDFromName
(
request
.
Name
)
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"invalid user name: %v"
,
err
)
}
user
,
err
:=
s
.
Store
.
GetUser
(
ctx
,
&
store
.
FindUser
{
ID
:
&
userID
})
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to get user: %v"
,
err
)
}
currentUser
,
err
:=
s
.
GetCurrentUser
(
ctx
)
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to get user: %v"
,
err
)
}
// For unauthenticated users, only public memos are visible.
visibilities
:=
[]
store
.
Visibility
{
store
.
Public
}
if
currentUser
!=
nil
{
// For authenticated users, protected memos are also visible.
visibilities
=
append
(
visibilities
,
store
.
Protected
)
if
currentUser
.
ID
==
user
.
ID
{
// For the current user, show all memos including private ones.
visibilities
=
[]
store
.
Visibility
{
store
.
Public
,
store
.
Protected
,
store
.
Private
}
}
}
workspaceMemoRelatedSetting
,
err
:=
s
.
Store
.
GetWorkspaceMemoRelatedSetting
(
ctx
)
if
err
!=
nil
{
return
nil
,
errors
.
Wrap
(
err
,
"failed to get workspace memo related setting"
)
}
userStats
:=
&
v1pb
.
UserStats
{
Name
:
fmt
.
Sprintf
(
"%s%d"
,
UserNamePrefix
,
user
.
ID
),
MemoDisplayTimestamps
:
[]
*
timestamppb
.
Timestamp
{},
MemoTypeStats
:
&
v1pb
.
UserStats_MemoTypeStats
{},
TagCount
:
map
[
string
]
int32
{},
}
memoFind
:=
&
store
.
FindMemo
{
// Exclude comments by default.
ExcludeComments
:
true
,
ExcludeContent
:
true
,
}
if
err
:=
s
.
buildMemoFindWithFilter
(
ctx
,
memoFind
,
request
.
Filter
);
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"failed to build find memos with filter: %v"
,
err
)
}
// Override the creator ID and visibility list.
memoFind
.
CreatorID
=
&
user
.
ID
memoFind
.
VisibilityList
=
visibilities
memos
,
err
:=
s
.
Store
.
ListMemos
(
ctx
,
memoFind
)
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to list memos: %v"
,
err
)
}
for
_
,
memo
:=
range
memos
{
displayTs
:=
memo
.
CreatedTs
if
workspaceMemoRelatedSetting
.
DisplayWithUpdateTime
{
displayTs
=
memo
.
UpdatedTs
}
userStats
.
MemoDisplayTimestamps
=
append
(
userStats
.
MemoDisplayTimestamps
,
timestamppb
.
New
(
time
.
Unix
(
displayTs
,
0
)))
// Handle duplicated tags.
for
_
,
tag
:=
range
memo
.
Payload
.
Tags
{
userStats
.
TagCount
[
tag
]
++
}
if
memo
.
Payload
.
Property
.
GetHasLink
()
{
userStats
.
MemoTypeStats
.
LinkCount
++
}
if
memo
.
Payload
.
Property
.
GetHasTaskList
()
{
userStats
.
MemoTypeStats
.
TaskCount
++
}
if
memo
.
Payload
.
Property
.
GetHasCode
()
{
userStats
.
MemoTypeStats
.
CodeCount
++
}
}
return
userStats
,
nil
}
func
getDefaultUserSetting
(
workspaceMemoRelatedSetting
*
storepb
.
WorkspaceMemoRelatedSetting
)
*
v1pb
.
UserSetting
{
defaultVisibility
:=
"PRIVATE"
if
workspaceMemoRelatedSetting
.
DefaultVisibility
!=
""
{
...
...
server/router/api/v1/user_service_stats.go
0 → 100644
View file @
ee96465b
package
v1
import
(
"context"
"fmt"
"slices"
"time"
"github.com/pkg/errors"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"
v1pb
"github.com/usememos/memos/proto/gen/api/v1"
"github.com/usememos/memos/store"
)
func
(
s
*
APIV1Service
)
ListAllUserStats
(
ctx
context
.
Context
,
request
*
v1pb
.
ListAllUserStatsRequest
)
(
*
v1pb
.
ListAllUserStatsResponse
,
error
)
{
users
,
err
:=
s
.
Store
.
ListUsers
(
ctx
,
&
store
.
FindUser
{})
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to list users: %v"
,
err
)
}
userStatsList
:=
[]
*
v1pb
.
UserStats
{}
for
_
,
user
:=
range
users
{
userStats
,
err
:=
s
.
GetUserStats
(
ctx
,
&
v1pb
.
GetUserStatsRequest
{
Name
:
fmt
.
Sprintf
(
"%s%d"
,
UserNamePrefix
,
user
.
ID
),
Filter
:
request
.
Filter
,
})
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to get user stats: %v"
,
err
)
}
userStatsList
=
append
(
userStatsList
,
userStats
)
}
return
&
v1pb
.
ListAllUserStatsResponse
{
UserStats
:
userStatsList
,
},
nil
}
func
(
s
*
APIV1Service
)
GetUserStats
(
ctx
context
.
Context
,
request
*
v1pb
.
GetUserStatsRequest
)
(
*
v1pb
.
UserStats
,
error
)
{
userID
,
err
:=
ExtractUserIDFromName
(
request
.
Name
)
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"invalid user name: %v"
,
err
)
}
user
,
err
:=
s
.
Store
.
GetUser
(
ctx
,
&
store
.
FindUser
{
ID
:
&
userID
})
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to get user: %v"
,
err
)
}
workspaceMemoRelatedSetting
,
err
:=
s
.
Store
.
GetWorkspaceMemoRelatedSetting
(
ctx
)
if
err
!=
nil
{
return
nil
,
errors
.
Wrap
(
err
,
"failed to get workspace memo related setting"
)
}
userStats
:=
&
v1pb
.
UserStats
{
Name
:
fmt
.
Sprintf
(
"%s%d"
,
UserNamePrefix
,
user
.
ID
),
MemoDisplayTimestamps
:
[]
*
timestamppb
.
Timestamp
{},
MemoTypeStats
:
&
v1pb
.
UserStats_MemoTypeStats
{},
TagCount
:
map
[
string
]
int32
{},
}
memoFind
:=
&
store
.
FindMemo
{
// Exclude comments by default.
ExcludeComments
:
true
,
ExcludeContent
:
true
,
}
if
err
:=
s
.
buildMemoFindWithFilter
(
ctx
,
memoFind
,
request
.
Filter
);
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"failed to build find memos with filter: %v"
,
err
)
}
currentUser
,
err
:=
s
.
GetCurrentUser
(
ctx
)
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to get user: %v"
,
err
)
}
if
len
(
memoFind
.
VisibilityList
)
==
0
{
visibilities
:=
[]
store
.
Visibility
{
store
.
Public
}
if
currentUser
!=
nil
{
visibilities
=
append
(
visibilities
,
store
.
Protected
)
if
currentUser
.
ID
==
user
.
ID
{
visibilities
=
append
(
visibilities
,
store
.
Private
)
}
}
memoFind
.
VisibilityList
=
visibilities
}
else
{
if
slices
.
Contains
(
memoFind
.
VisibilityList
,
store
.
Private
)
{
if
currentUser
==
nil
||
currentUser
.
ID
!=
user
.
ID
{
return
nil
,
status
.
Errorf
(
codes
.
PermissionDenied
,
"permission denied"
)
}
}
if
slices
.
Contains
(
memoFind
.
VisibilityList
,
store
.
Protected
)
{
if
currentUser
==
nil
{
return
nil
,
status
.
Errorf
(
codes
.
PermissionDenied
,
"permission denied"
)
}
}
}
// Override the creator ID.
memoFind
.
CreatorID
=
&
user
.
ID
memos
,
err
:=
s
.
Store
.
ListMemos
(
ctx
,
memoFind
)
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to list memos: %v"
,
err
)
}
for
_
,
memo
:=
range
memos
{
displayTs
:=
memo
.
CreatedTs
if
workspaceMemoRelatedSetting
.
DisplayWithUpdateTime
{
displayTs
=
memo
.
UpdatedTs
}
userStats
.
MemoDisplayTimestamps
=
append
(
userStats
.
MemoDisplayTimestamps
,
timestamppb
.
New
(
time
.
Unix
(
displayTs
,
0
)))
// Handle duplicated tags.
for
_
,
tag
:=
range
memo
.
Payload
.
Tags
{
userStats
.
TagCount
[
tag
]
++
}
if
memo
.
Payload
.
Property
.
GetHasLink
()
{
userStats
.
MemoTypeStats
.
LinkCount
++
}
if
memo
.
Payload
.
Property
.
GetHasCode
()
{
userStats
.
MemoTypeStats
.
CodeCount
++
}
if
memo
.
Payload
.
Property
.
GetHasTaskList
()
{
userStats
.
MemoTypeStats
.
TodoCount
++
}
if
memo
.
Payload
.
Property
.
GetHasIncompleteTasks
()
{
userStats
.
MemoTypeStats
.
UndoCount
++
}
}
return
userStats
,
nil
}
web/src/components/ExploreSidebar/ExploreSidebar.tsx
View file @
ee96465b
import
clsx
from
"clsx"
;
import
{
useLocation
}
from
"react-router-dom"
;
import
useDebounce
from
"react-use/lib/useDebounce"
;
import
SearchBar
from
"@/components/SearchBar"
;
import
{
useMemoList
,
useMemoMetadataStore
}
from
"@/store/v1"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
{
useUserStatsStore
}
from
"@/store/v1"
;
import
TagsSection
from
"../HomeSidebar/TagsSection"
;
import
StatisticsView
from
"../StatisticsView"
;
interface
Props
{
className
?:
string
;
}
const
ExploreSidebar
=
(
props
:
Props
)
=>
{
const
location
=
useLocation
();
const
memoList
=
useMemoList
();
const
memoMetadataStore
=
useMemoMetadataStore
();
const
currentUser
=
useCurrentUser
();
const
userStatsStore
=
useUserStatsStore
();
useDebounce
(
async
()
=>
{
if
(
memoList
.
size
()
===
0
)
return
;
await
memoMetadataStore
.
fetchMemoMetadata
({
location
}
);
const
filters
=
[
`state == "NORMAL"`
,
`visibilities == [
${
currentUser
?
"'PUBLIC', 'PROTECTED'"
:
"'PUBLIC'"
}
]`
]
;
userStatsStore
.
listUserStats
(
undefined
,
filters
.
join
(
" && "
)
);
},
300
,
[
memoList
.
size
(),
location
.
pathname
],
[],
);
return
(
...
...
@@ -31,6 +31,7 @@ const ExploreSidebar = (props: Props) => {
)
}
>
<
SearchBar
/>
<
StatisticsView
/>
<
TagsSection
readonly=
{
true
}
/>
</
aside
>
);
...
...
web/src/components/HomeSidebar/HomeSidebar.tsx
View file @
ee96465b
import
clsx
from
"clsx"
;
import
{
useLocation
}
from
"react-router-dom"
;
import
useDebounce
from
"react-use/lib/useDebounce"
;
import
SearchBar
from
"@/components/SearchBar"
;
import
UserStatisticsView
from
"@/components/User
StatisticsView"
;
import
StatisticsView
from
"@/components/
StatisticsView"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
{
useMemoList
,
use
MemoMetadata
Store
}
from
"@/store/v1"
;
import
{
useMemoList
,
use
UserStats
Store
}
from
"@/store/v1"
;
import
TagsSection
from
"./TagsSection"
;
interface
Props
{
...
...
@@ -12,17 +11,17 @@ interface Props {
}
const
HomeSidebar
=
(
props
:
Props
)
=>
{
const
location
=
useLocation
();
const
user
=
useCurrentUser
();
const
currentUser
=
useCurrentUser
();
const
memoList
=
useMemoList
();
const
memoMetadataStore
=
useMemoMetadata
Store
();
const
userStatsStore
=
useUserStats
Store
();
useDebounce
(
async
()
=>
{
await
memoMetadataStore
.
fetchMemoMetadata
({
user
,
location
});
const
filters
=
[
`state == "NORMAL"`
];
await
userStatsStore
.
listUserStats
(
currentUser
.
name
,
filters
.
join
(
" && "
));
},
300
,
[
memoList
.
size
(),
user
,
location
.
pathname
],
[
memoList
.
size
(),
currentUser
],
);
return
(
...
...
@@ -33,7 +32,7 @@ const HomeSidebar = (props: Props) => {
)
}
>
<
SearchBar
/>
<
User
StatisticsView
/>
<
StatisticsView
/>
<
TagsSection
/>
</
aside
>
);
...
...
web/src/components/HomeSidebar/TagsSection.tsx
View file @
ee96465b
...
...
@@ -2,11 +2,10 @@ import { Dropdown, Menu, MenuButton, MenuItem, Switch } from "@mui/joy";
import
clsx
from
"clsx"
;
import
{
Edit3Icon
,
HashIcon
,
MoreVerticalIcon
,
TagsIcon
,
TrashIcon
}
from
"lucide-react"
;
import
toast
from
"react-hot-toast"
;
import
{
useLocation
}
from
"react-router-dom"
;
import
useLocalStorage
from
"react-use/lib/useLocalStorage"
;
import
{
memoServiceClient
}
from
"@/grpcweb"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
{
useMemoFilterStore
,
use
MemoMetadataStore
,
useMemoTagList
}
from
"@/store/v1"
;
import
{
useMemoFilterStore
,
use
UserStatsStore
,
useUserStatsTags
}
from
"@/store/v1"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
showRenameTagDialog
from
"../RenameTagDialog"
;
import
TagTree
from
"../TagTree"
;
...
...
@@ -18,12 +17,11 @@ interface Props {
const
TagsSection
=
(
props
:
Props
)
=>
{
const
t
=
useTranslate
();
const
location
=
useLocation
();
const
user
=
useCurrentUser
();
const
currentUser
=
useCurrentUser
();
const
memoFilterStore
=
useMemoFilterStore
();
const
memoMetadataStore
=
useMemoMetadata
Store
();
const
userStatsStore
=
useUserStats
Store
();
const
[
treeMode
,
setTreeMode
]
=
useLocalStorage
<
boolean
>
(
"tag-view-as-tree"
,
false
);
const
tags
=
Object
.
entries
(
use
MemoTagList
())
const
tags
=
Object
.
entries
(
use
UserStatsTags
())
.
sort
((
a
,
b
)
=>
a
[
0
].
localeCompare
(
b
[
0
]))
.
sort
((
a
,
b
)
=>
b
[
1
]
-
a
[
1
]);
...
...
@@ -46,7 +44,7 @@ const TagsSection = (props: Props) => {
parent
:
"memos/-"
,
tag
:
tag
,
});
await
memoMetadataStore
.
fetchMemoMetadata
({
user
,
location
}
);
await
userStatsStore
.
listUserStats
(
currentUser
.
name
);
toast
.
success
(
t
(
"message.deleted-successfully"
));
}
};
...
...
web/src/components/MemoEditor/ActionButton/TagSelector.tsx
View file @
ee96465b
...
...
@@ -4,7 +4,7 @@ import { HashIcon } from "lucide-react";
import
{
useRef
,
useState
}
from
"react"
;
import
useClickAway
from
"react-use/lib/useClickAway"
;
import
OverflowTip
from
"@/components/kit/OverflowTip"
;
import
{
use
MemoTagList
}
from
"@/store/v1"
;
import
{
use
UserStatsTags
}
from
"@/store/v1"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
EditorRefActions
}
from
"../Editor"
;
...
...
@@ -17,7 +17,7 @@ const TagSelector = (props: Props) => {
const
{
editorRef
}
=
props
;
const
[
open
,
setOpen
]
=
useState
(
false
);
const
containerRef
=
useRef
<
HTMLDivElement
>
(
null
);
const
tags
=
Object
.
entries
(
use
MemoTagList
())
const
tags
=
Object
.
entries
(
use
UserStatsTags
())
.
sort
((
a
,
b
)
=>
a
[
0
].
localeCompare
(
b
[
0
]))
.
sort
((
a
,
b
)
=>
b
[
1
]
-
a
[
1
])
.
map
(([
tag
])
=>
tag
);
...
...
web/src/components/MemoEditor/Editor/TagSuggestions.tsx
View file @
ee96465b
...
...
@@ -3,7 +3,7 @@ import Fuse from "fuse.js";
import
{
useEffect
,
useRef
,
useState
}
from
"react"
;
import
getCaretCoordinates
from
"textarea-caret"
;
import
OverflowTip
from
"@/components/kit/OverflowTip"
;
import
{
use
MemoTagList
}
from
"@/store/v1"
;
import
{
use
UserStatsTags
}
from
"@/store/v1"
;
import
{
EditorRefActions
}
from
"."
;
type
Props
=
{
...
...
@@ -18,7 +18,7 @@ const TagSuggestions = ({ editorRef, editorActions }: Props) => {
const
[
selected
,
select
]
=
useState
(
0
);
const
selectedRef
=
useRef
(
selected
);
selectedRef
.
current
=
selected
;
const
tags
=
Object
.
entries
(
use
MemoTagList
())
const
tags
=
Object
.
entries
(
use
UserStatsTags
())
.
sort
((
a
,
b
)
=>
a
[
0
].
localeCompare
(
b
[
0
]))
.
sort
((
a
,
b
)
=>
b
[
1
]
-
a
[
1
])
.
map
(([
tag
])
=>
tag
);
...
...
web/src/components/PagedMemoList/PagedMemoList.tsx
View file @
ee96465b
import
{
Button
}
from
"@usememos/mui"
;
import
{
ArrowDownIcon
,
ArrowUpIcon
,
LoaderIcon
,
SlashIcon
}
from
"lucide-react"
;
import
{
useEffect
,
use
Memo
,
useRef
,
use
State
}
from
"react"
;
import
{
useEffect
,
useState
}
from
"react"
;
import
PullToRefresh
from
"react-simple-pull-to-refresh"
;
import
{
DEFAULT_LIST_MEMOS_PAGE_SIZE
}
from
"@/helpers/consts"
;
import
useResponsiveWidth
from
"@/hooks/useResponsiveWidth"
;
import
{
Routes
}
from
"@/router"
;
import
{
useMemoList
,
useMemoStore
}
from
"@/store/v1"
;
import
{
Memo
}
from
"@/types/proto/api/v1/memo_service"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
...
...
@@ -27,15 +26,10 @@ const PagedMemoList = (props: Props) => {
const
{
md
}
=
useResponsiveWidth
();
const
memoStore
=
useMemoStore
();
const
memoList
=
useMemoList
();
const
containerRef
=
useRef
<
HTMLDivElement
>
(
null
);
const
[
state
,
setState
]
=
useState
<
State
>
({
isRequesting
:
true
,
// Initial request
nextPageToken
:
""
,
});
const
shouldShowBackToTop
=
useMemo
(
()
=>
[
Routes
.
ROOT
,
Routes
.
EXPLORE
,
Routes
.
ARCHIVED
].
includes
(
location
.
pathname
as
Routes
)
||
location
.
pathname
.
startsWith
(
"/u/"
),
[
location
.
pathname
],
);
const
sortedMemoList
=
props
.
listSort
?
props
.
listSort
(
memoList
.
value
)
:
memoList
.
value
;
const
fetchMoreMemos
=
async
(
nextPageToken
:
string
)
=>
{
...
...
@@ -62,7 +56,7 @@ const PagedMemoList = (props: Props) => {
},
[
props
.
filter
,
props
.
pageSize
]);
const
children
=
(
<
div
ref=
{
containerRef
}
className=
"flex flex-col justify-start items-start w-full max-w-full"
>
<
div
className=
"flex flex-col justify-start items-start w-full max-w-full"
>
{
sortedMemoList
.
map
((
memo
)
=>
props
.
renderer
(
memo
))
}
{
state
.
isRequesting
&&
(
<
div
className=
"w-full flex flex-row justify-center items-center my-4"
>
...
...
@@ -84,10 +78,10 @@ const PagedMemoList = (props: Props) => {
{
t
(
"memo.load-more"
)
}
<
ArrowDownIcon
className=
"ml-1 w-4 h-auto"
/>
</
Button
>
{
shouldShowBackToTop
&&
<
SlashIcon
className=
"mx-1 w-4 h-auto opacity-40"
/>
}
<
SlashIcon
className=
"mx-1 w-4 h-auto opacity-40"
/>
</>
)
}
{
shouldShowBackToTop
&&
<
BackToTop
/>
}
<
BackToTop
/>
</
div
>
)
}
</>
...
...
web/src/components/RenameTagDialog.tsx
View file @
ee96465b
...
...
@@ -6,7 +6,7 @@ import { toast } from "react-hot-toast";
import
{
memoServiceClient
}
from
"@/grpcweb"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
useLoading
from
"@/hooks/useLoading"
;
import
{
use
MemoMetadata
Store
}
from
"@/store/v1"
;
import
{
use
UserStats
Store
}
from
"@/store/v1"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
generateDialog
}
from
"./Dialog"
;
...
...
@@ -17,7 +17,7 @@ interface Props extends DialogProps {
const
RenameTagDialog
:
React
.
FC
<
Props
>
=
(
props
:
Props
)
=>
{
const
{
tag
,
destroy
}
=
props
;
const
t
=
useTranslate
();
const
memoMetadataStore
=
useMemoMetadata
Store
();
const
userStatsStore
=
useUserStats
Store
();
const
[
newName
,
setNewName
]
=
useState
(
tag
);
const
requestState
=
useLoading
(
false
);
const
user
=
useCurrentUser
();
...
...
@@ -43,7 +43,7 @@ const RenameTagDialog: React.FC<Props> = (props: Props) => {
newTag
:
newName
,
});
toast
.
success
(
"Rename tag successfully"
);
memoMetadataStore
.
fetchMemoMetadata
({
user
}
);
userStatsStore
.
listUserStats
(
user
.
name
);
}
catch
(
error
:
any
)
{
console
.
error
(
error
);
toast
.
error
(
error
.
details
);
...
...
web/src/components/
User
StatisticsView.tsx
→
web/src/components/StatisticsView.tsx
View file @
ee96465b
...
...
@@ -7,25 +7,18 @@ import { useState } from "react";
import
useAsyncEffect
from
"@/hooks/useAsyncEffect"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
i18n
from
"@/i18n"
;
import
{
useMemoFilterStore
,
useMemoMetadataStore
}
from
"@/store/v1"
;
import
{
useMemoFilterStore
,
useUserStatsStore
}
from
"@/store/v1"
;
import
{
UserStats_MemoTypeStats
}
from
"@/types/proto/api/v1/user_service"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
ActivityCalendar
from
"./ActivityCalendar"
;
interface
UserMemoStats
{
link
:
number
;
taskList
:
number
;
code
:
number
;
incompleteTasks
:
number
;
}
const
UserStatisticsView
=
()
=>
{
const
StatisticsView
=
()
=>
{
const
t
=
useTranslate
();
const
currentUser
=
useCurrentUser
();
const
memoFilterStore
=
useMemoFilterStore
();
const
memoMetadataStore
=
useMemoMetadataStore
();
const
metadataList
=
Object
.
values
(
memoMetadataStore
.
getState
().
dataMapByName
);
const
userStatsStore
=
useUserStatsStore
();
const
[
memoAmount
,
setMemoAmount
]
=
useState
(
0
);
const
[
memo
Stats
,
setMemoStats
]
=
useState
<
UserMemoStats
>
({
link
:
0
,
taskList
:
0
,
code
:
0
,
incompleteTasks
:
0
}
);
const
[
memo
TypeStats
,
setMemoTypeStats
]
=
useState
<
UserStats_MemoTypeStats
>
(
UserStats_MemoTypeStats
.
fromPartial
({})
);
const
[
activityStats
,
setActivityStats
]
=
useState
<
Record
<
string
,
number
>>
({});
const
[
selectedDate
]
=
useState
(
new
Date
());
const
[
visibleMonthString
,
setVisibleMonthString
]
=
useState
(
dayjs
(
selectedDate
.
toDateString
()).
format
(
"YYYY-MM"
));
...
...
@@ -35,26 +28,21 @@ const UserStatisticsView = () => {
const
singularOrPluralDay
=
(
days
>
0
?
t
(
"common.days"
)
:
t
(
"common.day"
)).
toLowerCase
();
useAsyncEffect
(
async
()
=>
{
const
memoStats
:
UserMemoStats
=
{
link
:
0
,
taskList
:
0
,
code
:
0
,
incompleteTasks
:
0
};
metadataList
.
forEach
((
memo
)
=>
{
const
{
property
}
=
memo
;
if
(
property
?.
hasLink
)
{
memoStats
.
link
+=
1
;
}
if
(
property
?.
hasTaskList
)
{
memoStats
.
taskList
+=
1
;
}
if
(
property
?.
hasCode
)
{
memoStats
.
code
+=
1
;
}
if
(
property
?.
hasIncompleteTasks
)
{
memoStats
.
incompleteTasks
+=
1
;
const
memoTypeStats
=
UserStats_MemoTypeStats
.
fromPartial
({});
const
displayTimeList
:
Date
[]
=
[];
for
(
const
stats
of
Object
.
values
(
userStatsStore
.
userStatsByName
))
{
displayTimeList
.
push
(...
stats
.
memoDisplayTimestamps
);
if
(
stats
.
memoTypeStats
)
{
memoTypeStats
.
codeCount
+=
stats
.
memoTypeStats
.
codeCount
;
memoTypeStats
.
linkCount
+=
stats
.
memoTypeStats
.
linkCount
;
memoTypeStats
.
todoCount
+=
stats
.
memoTypeStats
.
todoCount
;
memoTypeStats
.
undoCount
+=
stats
.
memoTypeStats
.
undoCount
;
}
}
);
setMemo
Stats
(
memo
Stats
);
setMemoAmount
(
metadata
List
.
length
);
setActivityStats
(
countBy
(
metadataList
.
map
((
memo
)
=>
dayjs
(
memo
.
displayTim
e
).
format
(
"YYYY-MM-DD"
))));
},
[
memoMetadata
Store
.
stateId
]);
}
setMemo
TypeStats
(
memoType
Stats
);
setMemoAmount
(
displayTime
List
.
length
);
setActivityStats
(
countBy
(
displayTimeList
.
map
((
date
)
=>
dayjs
(
dat
e
).
format
(
"YYYY-MM-DD"
))));
},
[
userStats
Store
.
stateId
]);
const
onCalendarClick
=
(
date
:
string
)
=>
{
memoFilterStore
.
removeFilter
((
f
)
=>
f
.
factor
===
"displayTime"
);
...
...
@@ -110,26 +98,26 @@ const UserStatisticsView = () => {
<
LinkIcon
className=
"w-4 h-auto mr-1"
/>
<
span
className=
"block text-sm"
>
{
t
(
"memo.links"
)
}
</
span
>
</
div
>
<
span
className=
"text-sm truncate"
>
{
memo
Stats
.
link
}
</
span
>
<
span
className=
"text-sm truncate"
>
{
memo
TypeStats
.
linkCount
}
</
span
>
</
div
>
<
div
className=
{
clsx
(
"w-auto border dark:border-zinc-800 pl-1 pr-1.5 rounded-md flex justify-between items-center"
)
}
onClick=
{
()
=>
memoFilterStore
.
addFilter
({
factor
:
"property.hasTaskList"
,
value
:
""
})
}
>
<
div
className=
"w-auto flex justify-start items-center mr-1"
>
{
memo
Stats
.
incompleteTasks
>
0
?
<
ListTodoIcon
className=
"w-4 h-auto mr-1"
/>
:
<
CheckCircleIcon
className=
"w-4 h-auto mr-1"
/>
}
{
memo
TypeStats
.
undoCount
>
0
?
<
ListTodoIcon
className=
"w-4 h-auto mr-1"
/>
:
<
CheckCircleIcon
className=
"w-4 h-auto mr-1"
/>
}
<
span
className=
"block text-sm"
>
{
t
(
"memo.to-do"
)
}
</
span
>
</
div
>
{
memo
Stats
.
incompleteTasks
>
0
?
(
{
memo
TypeStats
.
undoCount
>
0
?
(
<
Tooltip
title=
{
"Done / Total"
}
placement=
"top"
arrow
>
<
div
className=
"text-sm flex flex-row items-start justify-center"
>
<
span
className=
"truncate"
>
{
memo
Stats
.
taskList
-
memoStats
.
incompleteTasks
}
</
span
>
<
span
className=
"truncate"
>
{
memo
TypeStats
.
todoCount
-
memoTypeStats
.
undoCount
}
</
span
>
<
span
className=
"font-mono opacity-50"
>
/
</
span
>
<
span
className=
"truncate"
>
{
memo
Stats
.
taskLis
t
}
</
span
>
<
span
className=
"truncate"
>
{
memo
TypeStats
.
todoCoun
t
}
</
span
>
</
div
>
</
Tooltip
>
)
:
(
<
span
className=
"text-sm truncate"
>
{
memo
Stats
.
taskLis
t
}
</
span
>
<
span
className=
"text-sm truncate"
>
{
memo
TypeStats
.
todoCoun
t
}
</
span
>
)
}
</
div
>
<
div
...
...
@@ -140,11 +128,11 @@ const UserStatisticsView = () => {
<
Code2Icon
className=
"w-4 h-auto mr-1"
/>
<
span
className=
"block text-sm"
>
{
t
(
"memo.code"
)
}
</
span
>
</
div
>
<
span
className=
"text-sm truncate"
>
{
memo
Stats
.
code
}
</
span
>
<
span
className=
"text-sm truncate"
>
{
memo
TypeStats
.
codeCount
}
</
span
>
</
div
>
</
div
>
</
div
>
);
};
export
default
User
StatisticsView
;
export
default
StatisticsView
;
web/src/store/v1/index.ts
View file @
ee96465b
...
...
@@ -5,4 +5,4 @@ export * from "./resourceName";
export
*
from
"./resource"
;
export
*
from
"./workspaceSetting"
;
export
*
from
"./memoFilter"
;
export
*
from
"./
memoMetadata
"
;
export
*
from
"./
userStats
"
;
web/src/store/v1/memoMetadata.ts
deleted
100644 → 0
View file @
cde058c7
import
{
uniqueId
}
from
"lodash-es"
;
import
{
Location
}
from
"react-router-dom"
;
import
{
create
}
from
"zustand"
;
import
{
combine
}
from
"zustand/middleware"
;
import
{
memoServiceClient
}
from
"@/grpcweb"
;
import
{
Routes
}
from
"@/router"
;
import
{
Memo
,
MemoView
}
from
"@/types/proto/api/v1/memo_service"
;
import
{
User
}
from
"@/types/proto/api/v1/user_service"
;
// Set the maximum number of memos to fetch.
const
DEFAULT_MEMO_PAGE_SIZE
=
1000000
;
interface
State
{
// stateId is used to identify the store instance state.
// It should be update when any state change.
stateId
:
string
;
dataMapByName
:
Record
<
string
,
Memo
>
;
}
const
getDefaultState
=
():
State
=>
({
stateId
:
uniqueId
(),
dataMapByName
:
{},
});
export
const
useMemoMetadataStore
=
create
(
combine
(
getDefaultState
(),
(
set
,
get
)
=>
({
setState
:
(
state
:
State
)
=>
set
(
state
),
getState
:
()
=>
get
(),
fetchMemoMetadata
:
async
(
params
:
{
user
?:
User
;
location
?:
Location
<
any
>
})
=>
{
const
filters
=
[
`state == "NORMAL"`
];
if
(
params
.
user
)
{
if
(
params
.
location
?.
pathname
===
Routes
.
EXPLORE
)
{
filters
.
push
(
`visibilities == ["PUBLIC", "PROTECTED"]`
);
}
filters
.
push
(
`creator == "
${
params
.
user
.
name
}
"`
);
}
else
{
filters
.
push
(
`visibilities == ["PUBLIC"]`
);
}
const
{
memos
,
nextPageToken
}
=
await
memoServiceClient
.
listMemos
({
filter
:
filters
.
join
(
" && "
),
view
:
MemoView
.
MEMO_VIEW_METADATA_ONLY
,
pageSize
:
DEFAULT_MEMO_PAGE_SIZE
,
});
const
memoMap
=
memos
.
reduce
<
Record
<
string
,
Memo
>>
(
(
acc
,
memo
)
=>
({
...
acc
,
[
memo
.
name
]:
memo
,
}),
{},
);
set
({
stateId
:
uniqueId
(),
dataMapByName
:
memoMap
});
return
{
memos
,
nextPageToken
};
},
})),
);
export
const
useMemoTagList
=
()
=>
{
const
memoStore
=
useMemoMetadataStore
();
const
memos
=
Object
.
values
(
memoStore
.
getState
().
dataMapByName
);
const
tagAmounts
:
Record
<
string
,
number
>
=
{};
memos
.
forEach
((
memo
)
=>
{
const
tagSet
=
new
Set
<
string
>
();
for
(
const
tag
of
memo
.
tags
)
{
const
parts
=
tag
.
split
(
"/"
);
let
currentTag
=
""
;
for
(
const
part
of
parts
)
{
currentTag
=
currentTag
?
`
${
currentTag
}
/
${
part
}
`
:
part
;
tagSet
.
add
(
currentTag
);
}
}
Array
.
from
(
tagSet
).
forEach
((
tag
)
=>
{
tagAmounts
[
tag
]
=
tagAmounts
[
tag
]
?
tagAmounts
[
tag
]
+
1
:
1
;
});
});
return
tagAmounts
;
};
web/src/store/v1/userStats.ts
0 → 100644
View file @
ee96465b
import
{
uniqueId
}
from
"lodash-es"
;
import
{
create
}
from
"zustand"
;
import
{
combine
}
from
"zustand/middleware"
;
import
{
userServiceClient
}
from
"@/grpcweb"
;
import
{
UserStats
}
from
"@/types/proto/api/v1/user_service"
;
interface
State
{
// stateId is used to identify the store instance state.
// It should be update when any state change.
stateId
:
string
;
userStatsByName
:
Record
<
string
,
UserStats
>
;
}
const
getDefaultState
=
():
State
=>
({
stateId
:
uniqueId
(),
userStatsByName
:
{},
});
export
const
useUserStatsStore
=
create
(
combine
(
getDefaultState
(),
(
set
,
get
)
=>
({
setState
:
(
state
:
State
)
=>
set
(
state
),
getState
:
()
=>
get
(),
listUserStats
:
async
(
user
?:
string
,
filter
?:
string
)
=>
{
const
userStatsByName
:
Record
<
string
,
UserStats
>
=
{};
if
(
!
user
)
{
const
{
userStats
}
=
await
userServiceClient
.
listAllUserStats
({
filter
});
for
(
const
stats
of
userStats
)
{
userStatsByName
[
stats
.
name
]
=
stats
;
}
}
else
{
const
userStats
=
await
userServiceClient
.
getUserStats
({
name
:
user
,
filter
});
userStatsByName
[
user
]
=
userStats
;
}
set
({
stateId
:
uniqueId
(),
userStatsByName
});
},
})),
);
export
const
useUserStatsTags
=
()
=>
{
const
userStatsStore
=
useUserStatsStore
();
const
tagAmounts
:
Record
<
string
,
number
>
=
{};
for
(
const
userStats
of
Object
.
values
(
userStatsStore
.
getState
().
userStatsByName
))
{
for
(
const
tag
of
Object
.
keys
(
userStats
.
tagCount
))
{
tagAmounts
[
tag
]
=
(
tagAmounts
[
tag
]
||
0
)
+
userStats
.
tagCount
[
tag
];
}
}
return
tagAmounts
;
};
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