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
8d8e9b3b
Commit
8d8e9b3b
authored
Jun 16, 2025
by
Steven
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
refactor: shortcut service
parent
1ea4cc45
Changes
10
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
895 additions
and
360 deletions
+895
-360
shortcut_service.proto
proto/api/v1/shortcut_service.proto
+78
-25
shortcut_service.pb.go
proto/gen/api/v1/shortcut_service.pb.go
+177
-88
shortcut_service.pb.gw.go
proto/gen/api/v1/shortcut_service.pb.gw.go
+113
-55
shortcut_service_grpc.pb.go
proto/gen/api/v1/shortcut_service_grpc.pb.go
+40
-0
apidocs.swagger.yaml
proto/gen/apidocs.swagger.yaml
+136
-77
shortcuts_service.go
server/router/api/v1/shortcuts_service.go
+88
-14
CreateShortcutDialog.tsx
web/src/components/CreateShortcutDialog.tsx
+11
-5
ShortcutsSection.tsx
web/src/components/HomeSidebar/ShortcutsSection.tsx
+12
-6
Home.tsx
web/src/pages/Home.tsx
+8
-1
shortcut_service.ts
web/src/types/proto/api/v1/shortcut_service.ts
+232
-89
No files found.
proto/api/v1/shortcut_service.proto
View file @
8d8e9b3b
...
...
@@ -4,6 +4,8 @@ package memos.api.v1;
import
"google/api/annotations.proto"
;
import
"google/api/client.proto"
;
import
"google/api/field_behavior.proto"
;
import
"google/api/resource.proto"
;
import
"google/protobuf/empty.proto"
;
import
"google/protobuf/field_mask.proto"
;
...
...
@@ -16,6 +18,12 @@ service ShortcutService {
option
(
google.api.method_signature
)
=
"parent"
;
}
// GetShortcut gets a shortcut by name.
rpc
GetShortcut
(
GetShortcutRequest
)
returns
(
Shortcut
)
{
option
(
google.api.http
)
=
{
get
:
"/api/v1/{name=users/*/shortcuts/*}"
};
option
(
google.api.method_signature
)
=
"name"
;
}
// CreateShortcut creates a new shortcut for a user.
rpc
CreateShortcut
(
CreateShortcutRequest
)
returns
(
Shortcut
)
{
option
(
google.api.http
)
=
{
...
...
@@ -28,56 +36,101 @@ service ShortcutService {
// UpdateShortcut updates a shortcut for a user.
rpc
UpdateShortcut
(
UpdateShortcutRequest
)
returns
(
Shortcut
)
{
option
(
google.api.http
)
=
{
patch
:
"/api/v1/{
parent=users/*}/shortcuts/{shortcut.id
}"
patch
:
"/api/v1/{
shortcut.name=users/*/shortcuts/*
}"
body
:
"shortcut"
};
option
(
google.api.method_signature
)
=
"
parent,
shortcut,update_mask"
;
option
(
google.api.method_signature
)
=
"shortcut,update_mask"
;
}
// DeleteShortcut deletes a shortcut for a user.
rpc
DeleteShortcut
(
DeleteShortcutRequest
)
returns
(
google.protobuf.Empty
)
{
option
(
google.api.http
)
=
{
delete
:
"/api/v1/{
parent=users/*}/shortcuts/{id
}"
};
option
(
google.api.method_signature
)
=
"
parent,id
"
;
option
(
google.api.http
)
=
{
delete
:
"/api/v1/{
name=users/*/shortcuts/*
}"
};
option
(
google.api.method_signature
)
=
"
name
"
;
}
}
message
Shortcut
{
string
id
=
1
;
string
title
=
2
;
string
filter
=
3
;
option
(
google.api.resource
)
=
{
type
:
"memos.api.v1/Shortcut"
pattern
:
"users/{user}/shortcuts/{shortcut}"
singular
:
"shortcut"
plural
:
"shortcuts"
};
// The resource name of the shortcut.
// Format: users/{user}/shortcuts/{shortcut}
string
name
=
1
[(
google.api.field_behavior
)
=
IDENTIFIER
];
// The title of the shortcut.
string
title
=
2
[(
google.api.field_behavior
)
=
REQUIRED
];
// The filter expression for the shortcut.
string
filter
=
3
[(
google.api.field_behavior
)
=
OPTIONAL
];
}
message
ListShortcutsRequest
{
// The name of the user.
string
parent
=
1
;
// Required. The parent resource where shortcuts are listed.
// Format: users/{user}
string
parent
=
1
[
(
google.api.field_behavior
)
=
REQUIRED
,
(
google.api.resource_reference
)
=
{
child_type
:
"memos.api.v1/Shortcut"
}
];
// Optional. The maximum number of shortcuts to return.
int32
page_size
=
2
[(
google.api.field_behavior
)
=
OPTIONAL
];
// Optional. A page token for pagination.
string
page_token
=
3
[(
google.api.field_behavior
)
=
OPTIONAL
];
}
message
ListShortcutsResponse
{
// The list of shortcuts.
repeated
Shortcut
shortcuts
=
1
;
}
message
CreateShortcutRequest
{
// The name of the user.
string
parent
=
1
;
// A token for the next page of results.
string
next_page_token
=
2
;
// The total count of shortcuts.
int32
total_size
=
3
;
}
Shortcut
shortcut
=
2
;
message
GetShortcutRequest
{
// Required. The resource name of the shortcut to retrieve.
// Format: users/{user}/shortcuts/{shortcut}
string
name
=
1
[
(
google.api.field_behavior
)
=
REQUIRED
,
(
google.api.resource_reference
)
=
{
type
:
"memos.api.v1/Shortcut"
}
];
}
bool
validate_only
=
3
;
message
CreateShortcutRequest
{
// Required. The parent resource where this shortcut will be created.
// Format: users/{user}
string
parent
=
1
[
(
google.api.field_behavior
)
=
REQUIRED
,
(
google.api.resource_reference
)
=
{
child_type
:
"memos.api.v1/Shortcut"
}
];
// Required. The shortcut to create.
Shortcut
shortcut
=
2
[(
google.api.field_behavior
)
=
REQUIRED
];
// Optional. If set, validate the request, but do not actually create the shortcut.
bool
validate_only
=
3
[(
google.api.field_behavior
)
=
OPTIONAL
];
}
message
UpdateShortcutRequest
{
//
The name of the us
er.
string
parent
=
1
;
//
Required. The shortcut resource which replaces the resource on the serv
er.
Shortcut
shortcut
=
1
[(
google.api.field_behavior
)
=
REQUIRED
]
;
Shortcut
shortcut
=
2
;
google.protobuf.FieldMask
update_mask
=
3
;
// Optional. The list of fields to update.
google.protobuf.FieldMask
update_mask
=
2
[(
google.api.field_behavior
)
=
OPTIONAL
];
}
message
DeleteShortcutRequest
{
// The name of the user.
string
parent
=
1
;
// The id of the shortcut.
string
id
=
2
;
// Required. The resource name of the shortcut to delete.
// Format: users/{user}/shortcuts/{shortcut}
string
name
=
1
[
(
google.api.field_behavior
)
=
REQUIRED
,
(
google.api.resource_reference
)
=
{
type
:
"memos.api.v1/Shortcut"
}
];
}
proto/gen/api/v1/shortcut_service.pb.go
View file @
8d8e9b3b
This diff is collapsed.
Click to expand it.
proto/gen/api/v1/shortcut_service.pb.gw.go
View file @
8d8e9b3b
This diff is collapsed.
Click to expand it.
proto/gen/api/v1/shortcut_service_grpc.pb.go
View file @
8d8e9b3b
...
...
@@ -21,6 +21,7 @@ const _ = grpc.SupportPackageIsVersion9
const
(
ShortcutService_ListShortcuts_FullMethodName
=
"/memos.api.v1.ShortcutService/ListShortcuts"
ShortcutService_GetShortcut_FullMethodName
=
"/memos.api.v1.ShortcutService/GetShortcut"
ShortcutService_CreateShortcut_FullMethodName
=
"/memos.api.v1.ShortcutService/CreateShortcut"
ShortcutService_UpdateShortcut_FullMethodName
=
"/memos.api.v1.ShortcutService/UpdateShortcut"
ShortcutService_DeleteShortcut_FullMethodName
=
"/memos.api.v1.ShortcutService/DeleteShortcut"
...
...
@@ -32,6 +33,8 @@ const (
type
ShortcutServiceClient
interface
{
// ListShortcuts returns a list of shortcuts for a user.
ListShortcuts
(
ctx
context
.
Context
,
in
*
ListShortcutsRequest
,
opts
...
grpc
.
CallOption
)
(
*
ListShortcutsResponse
,
error
)
// GetShortcut gets a shortcut by name.
GetShortcut
(
ctx
context
.
Context
,
in
*
GetShortcutRequest
,
opts
...
grpc
.
CallOption
)
(
*
Shortcut
,
error
)
// CreateShortcut creates a new shortcut for a user.
CreateShortcut
(
ctx
context
.
Context
,
in
*
CreateShortcutRequest
,
opts
...
grpc
.
CallOption
)
(
*
Shortcut
,
error
)
// UpdateShortcut updates a shortcut for a user.
...
...
@@ -58,6 +61,16 @@ func (c *shortcutServiceClient) ListShortcuts(ctx context.Context, in *ListShort
return
out
,
nil
}
func
(
c
*
shortcutServiceClient
)
GetShortcut
(
ctx
context
.
Context
,
in
*
GetShortcutRequest
,
opts
...
grpc
.
CallOption
)
(
*
Shortcut
,
error
)
{
cOpts
:=
append
([]
grpc
.
CallOption
{
grpc
.
StaticMethod
()},
opts
...
)
out
:=
new
(
Shortcut
)
err
:=
c
.
cc
.
Invoke
(
ctx
,
ShortcutService_GetShortcut_FullMethodName
,
in
,
out
,
cOpts
...
)
if
err
!=
nil
{
return
nil
,
err
}
return
out
,
nil
}
func
(
c
*
shortcutServiceClient
)
CreateShortcut
(
ctx
context
.
Context
,
in
*
CreateShortcutRequest
,
opts
...
grpc
.
CallOption
)
(
*
Shortcut
,
error
)
{
cOpts
:=
append
([]
grpc
.
CallOption
{
grpc
.
StaticMethod
()},
opts
...
)
out
:=
new
(
Shortcut
)
...
...
@@ -94,6 +107,8 @@ func (c *shortcutServiceClient) DeleteShortcut(ctx context.Context, in *DeleteSh
type
ShortcutServiceServer
interface
{
// ListShortcuts returns a list of shortcuts for a user.
ListShortcuts
(
context
.
Context
,
*
ListShortcutsRequest
)
(
*
ListShortcutsResponse
,
error
)
// GetShortcut gets a shortcut by name.
GetShortcut
(
context
.
Context
,
*
GetShortcutRequest
)
(
*
Shortcut
,
error
)
// CreateShortcut creates a new shortcut for a user.
CreateShortcut
(
context
.
Context
,
*
CreateShortcutRequest
)
(
*
Shortcut
,
error
)
// UpdateShortcut updates a shortcut for a user.
...
...
@@ -113,6 +128,9 @@ type UnimplementedShortcutServiceServer struct{}
func
(
UnimplementedShortcutServiceServer
)
ListShortcuts
(
context
.
Context
,
*
ListShortcutsRequest
)
(
*
ListShortcutsResponse
,
error
)
{
return
nil
,
status
.
Errorf
(
codes
.
Unimplemented
,
"method ListShortcuts not implemented"
)
}
func
(
UnimplementedShortcutServiceServer
)
GetShortcut
(
context
.
Context
,
*
GetShortcutRequest
)
(
*
Shortcut
,
error
)
{
return
nil
,
status
.
Errorf
(
codes
.
Unimplemented
,
"method GetShortcut not implemented"
)
}
func
(
UnimplementedShortcutServiceServer
)
CreateShortcut
(
context
.
Context
,
*
CreateShortcutRequest
)
(
*
Shortcut
,
error
)
{
return
nil
,
status
.
Errorf
(
codes
.
Unimplemented
,
"method CreateShortcut not implemented"
)
}
...
...
@@ -161,6 +179,24 @@ func _ShortcutService_ListShortcuts_Handler(srv interface{}, ctx context.Context
return
interceptor
(
ctx
,
in
,
info
,
handler
)
}
func
_ShortcutService_GetShortcut_Handler
(
srv
interface
{},
ctx
context
.
Context
,
dec
func
(
interface
{})
error
,
interceptor
grpc
.
UnaryServerInterceptor
)
(
interface
{},
error
)
{
in
:=
new
(
GetShortcutRequest
)
if
err
:=
dec
(
in
);
err
!=
nil
{
return
nil
,
err
}
if
interceptor
==
nil
{
return
srv
.
(
ShortcutServiceServer
)
.
GetShortcut
(
ctx
,
in
)
}
info
:=
&
grpc
.
UnaryServerInfo
{
Server
:
srv
,
FullMethod
:
ShortcutService_GetShortcut_FullMethodName
,
}
handler
:=
func
(
ctx
context
.
Context
,
req
interface
{})
(
interface
{},
error
)
{
return
srv
.
(
ShortcutServiceServer
)
.
GetShortcut
(
ctx
,
req
.
(
*
GetShortcutRequest
))
}
return
interceptor
(
ctx
,
in
,
info
,
handler
)
}
func
_ShortcutService_CreateShortcut_Handler
(
srv
interface
{},
ctx
context
.
Context
,
dec
func
(
interface
{})
error
,
interceptor
grpc
.
UnaryServerInterceptor
)
(
interface
{},
error
)
{
in
:=
new
(
CreateShortcutRequest
)
if
err
:=
dec
(
in
);
err
!=
nil
{
...
...
@@ -226,6 +262,10 @@ var ShortcutService_ServiceDesc = grpc.ServiceDesc{
MethodName
:
"ListShortcuts"
,
Handler
:
_ShortcutService_ListShortcuts_Handler
,
},
{
MethodName
:
"GetShortcut"
,
Handler
:
_ShortcutService_GetShortcut_Handler
,
},
{
MethodName
:
"CreateShortcut"
,
Handler
:
_ShortcutService_CreateShortcut_Handler
,
...
...
proto/gen/apidocs.swagger.yaml
View file @
8d8e9b3b
...
...
@@ -1083,6 +1083,52 @@ paths:
tags
:
-
ResourceService
/api/v1/{name_5}
:
get
:
summary
:
GetShortcut gets a shortcut by name.
operationId
:
ShortcutService_GetShortcut
responses
:
"
200"
:
description
:
A successful response.
schema
:
$ref
:
'
#/definitions/apiv1Shortcut'
default
:
description
:
An unexpected error response.
schema
:
$ref
:
'
#/definitions/googlerpcStatus'
parameters
:
-
name
:
name_5
description
:
|-
Required. The resource name of the shortcut to retrieve.
Format: users/{user}/shortcuts/{shortcut}
in: path
required: true
type: string
pattern: users/[^/]+/shortcuts/[^/]+
tags
:
-
ShortcutService
delete
:
summary
:
DeleteMemo deletes a memo.
operationId
:
MemoService_DeleteMemo
responses
:
"
200"
:
description
:
A successful response.
schema
:
type
:
object
properties
:
{}
default
:
description
:
An unexpected error response.
schema
:
$ref
:
'
#/definitions/googlerpcStatus'
parameters
:
-
name
:
name_5
description
:
The name of the memo.
in
:
path
required
:
true
type
:
string
pattern
:
memos/[^/]+
tags
:
-
MemoService
/api/v1/{name_6}
:
get
:
summary
:
GetWebhook gets a webhook by name.
operationId
:
WebhookService_GetWebhook
...
...
@@ -1096,7 +1142,7 @@ paths:
schema
:
$ref
:
'
#/definitions/googlerpcStatus'
parameters
:
-
name
:
name_
5
-
name
:
name_
6
description
:
|-
Required. The resource name of the webhook.
Format: webhooks/{webhook}
...
...
@@ -1114,8 +1160,8 @@ paths:
tags
:
-
WebhookService
delete
:
summary
:
Delete
Memo deletes a memo
.
operationId
:
MemoService_DeleteMemo
summary
:
Delete
Shortcut deletes a shortcut for a user
.
operationId
:
ShortcutService_DeleteShortcut
responses
:
"
200"
:
description
:
A successful response.
...
...
@@ -1127,15 +1173,17 @@ paths:
schema
:
$ref
:
'
#/definitions/googlerpcStatus'
parameters
:
-
name
:
name_5
description
:
The name of the memo.
-
name
:
name_6
description
:
|-
Required. The resource name of the shortcut to delete.
Format: users/{user}/shortcuts/{shortcut}
in: path
required: true
type: string
pattern
:
memo
s/[^/]+
pattern:
users/[^/]+/shortcut
s/[^/]+
tags
:
-
Memo
Service
/api/v1/{name_
6
}
:
-
Shortcut
Service
/api/v1/{name_
7
}
:
get
:
summary
:
Gets a workspace setting.
operationId
:
WorkspaceService_GetWorkspaceSetting
...
...
@@ -1149,7 +1197,7 @@ paths:
schema
:
$ref
:
'
#/definitions/googlerpcStatus'
parameters
:
-
name
:
name_
6
-
name
:
name_
7
description
:
|-
The resource name of the workspace setting.
Format: workspace/settings/{setting}
...
...
@@ -1173,7 +1221,7 @@ paths:
schema
:
$ref
:
'
#/definitions/googlerpcStatus'
parameters
:
-
name
:
name_
6
-
name
:
name_
7
description
:
|-
Required. The resource name of the webhook to delete.
Format: webhooks/{webhook}
...
...
@@ -1676,11 +1724,24 @@ paths:
$ref
:
'
#/definitions/googlerpcStatus'
parameters
:
-
name
:
parent
description
:
The name of the user.
description
:
|-
Required. The parent resource where shortcuts are listed.
Format: users/{user}
in: path
required: true
type: string
pattern: users/[^/]+
-
name
:
pageSize
description
:
Optional. The maximum number of shortcuts to return.
in
:
query
required
:
false
type
:
integer
format
:
int32
-
name
:
pageToken
description
:
Optional. A page token for pagination.
in
:
query
required
:
false
type
:
string
tags
:
-
ShortcutService
post
:
...
...
@@ -1697,86 +1758,28 @@ paths:
$ref
:
'
#/definitions/googlerpcStatus'
parameters
:
-
name
:
parent
description
:
The name of the user.
description
:
|-
Required. The parent resource where this shortcut will be created.
Format: users/{user}
in: path
required: true
type: string
pattern: users/[^/]+
-
name
:
shortcut
description
:
Required. The shortcut to create.
in
:
body
required
:
true
schema
:
$ref
:
'
#/definitions/apiv1Shortcut'
required
:
-
shortcut
-
name
:
validateOnly
description
:
Optional. If set, validate the request, but do not actually create the shortcut.
in
:
query
required
:
false
type
:
boolean
tags
:
-
ShortcutService
/api/v1/{parent}/shortcuts/{id}
:
delete
:
summary
:
DeleteShortcut deletes a shortcut for a user.
operationId
:
ShortcutService_DeleteShortcut
responses
:
"
200"
:
description
:
A successful response.
schema
:
type
:
object
properties
:
{}
default
:
description
:
An unexpected error response.
schema
:
$ref
:
'
#/definitions/googlerpcStatus'
parameters
:
-
name
:
parent
description
:
The name of the user.
in
:
path
required
:
true
type
:
string
pattern
:
users/[^/]+
-
name
:
id
description
:
The id of the shortcut.
in
:
path
required
:
true
type
:
string
tags
:
-
ShortcutService
/api/v1/{parent}/shortcuts/{shortcut.id}
:
patch
:
summary
:
UpdateShortcut updates a shortcut for a user.
operationId
:
ShortcutService_UpdateShortcut
responses
:
"
200"
:
description
:
A successful response.
schema
:
$ref
:
'
#/definitions/apiv1Shortcut'
default
:
description
:
An unexpected error response.
schema
:
$ref
:
'
#/definitions/googlerpcStatus'
parameters
:
-
name
:
parent
description
:
The name of the user.
in
:
path
required
:
true
type
:
string
pattern
:
users/[^/]+
-
name
:
shortcut.id
in
:
path
required
:
true
type
:
string
-
name
:
shortcut
in
:
body
required
:
true
schema
:
type
:
object
properties
:
title
:
type
:
string
filter
:
type
:
string
tags
:
-
ShortcutService
/api/v1/{parent}/tags/{tag}
:
delete
:
summary
:
DeleteMemoTag deletes a tag for a memo.
...
...
@@ -1972,6 +1975,47 @@ paths:
-
setting
tags
:
-
UserService
/api/v1/{shortcut.name}
:
patch
:
summary
:
UpdateShortcut updates a shortcut for a user.
operationId
:
ShortcutService_UpdateShortcut
responses
:
"
200"
:
description
:
A successful response.
schema
:
$ref
:
'
#/definitions/apiv1Shortcut'
default
:
description
:
An unexpected error response.
schema
:
$ref
:
'
#/definitions/googlerpcStatus'
parameters
:
-
name
:
shortcut.name
description
:
|-
The resource name of the shortcut.
Format: users/{user}/shortcuts/{shortcut}
in: path
required: true
type: string
pattern: users/[^/]+/shortcuts/[^/]+
-
name
:
shortcut
description
:
Required. The shortcut resource which replaces the resource on the server.
in
:
body
required
:
true
schema
:
type
:
object
properties
:
title
:
type
:
string
description
:
The title of the shortcut.
filter
:
type
:
string
description
:
The filter expression for the shortcut.
title
:
Required. The shortcut resource which replaces the resource on the server.
required
:
-
title
-
shortcut
tags
:
-
ShortcutService
/api/v1/{user.name}
:
patch
:
summary
:
UpdateUser updates a user.
...
...
@@ -2479,12 +2523,19 @@ definitions:
apiv1Shortcut
:
type
:
object
properties
:
id
:
name
:
type
:
string
title
:
|-
The resource name of the shortcut.
Format: users/{user}/shortcuts/{shortcut}
title
:
type
:
string
description
:
The title of the shortcut.
filter
:
type
:
string
description
:
The filter expression for the shortcut.
required
:
-
title
apiv1UserSetting
:
type
:
object
properties
:
...
...
@@ -3072,6 +3123,14 @@ definitions:
items
:
type
:
object
$ref
:
'
#/definitions/apiv1Shortcut'
description
:
The list of shortcuts.
nextPageToken
:
type
:
string
description
:
A token for the next page of results.
totalSize
:
type
:
integer
format
:
int32
description
:
The total count of shortcuts.
v1ListUserAccessTokensResponse
:
type
:
object
properties
:
...
...
server/router/api/v1/shortcuts_service.go
View file @
8d8e9b3b
...
...
@@ -2,6 +2,8 @@ package v1
import
(
"context"
"fmt"
"strings"
"github.com/pkg/errors"
"google.golang.org/grpc/codes"
...
...
@@ -15,6 +17,32 @@ import (
"github.com/usememos/memos/store"
)
// Helper function to extract user ID and shortcut ID from shortcut resource name.
// Format: users/{user}/shortcuts/{shortcut}.
func
extractUserAndShortcutIDFromName
(
name
string
)
(
int32
,
string
,
error
)
{
parts
:=
strings
.
Split
(
name
,
"/"
)
if
len
(
parts
)
!=
4
||
parts
[
0
]
!=
"users"
||
parts
[
2
]
!=
"shortcuts"
{
return
0
,
""
,
errors
.
Errorf
(
"invalid shortcut name format: %s"
,
name
)
}
userID
,
err
:=
util
.
ConvertStringToInt32
(
parts
[
1
])
if
err
!=
nil
{
return
0
,
""
,
errors
.
Errorf
(
"invalid user ID %q"
,
parts
[
1
])
}
shortcutID
:=
parts
[
3
]
if
shortcutID
==
""
{
return
0
,
""
,
errors
.
Errorf
(
"empty shortcut ID in name: %s"
,
name
)
}
return
userID
,
shortcutID
,
nil
}
// Helper function to construct shortcut resource name.
func
constructShortcutName
(
userID
int32
,
shortcutID
string
)
string
{
return
fmt
.
Sprintf
(
"users/%d/shortcuts/%s"
,
userID
,
shortcutID
)
}
func
(
s
*
APIV1Service
)
ListShortcuts
(
ctx
context
.
Context
,
request
*
v1pb
.
ListShortcutsRequest
)
(
*
v1pb
.
ListShortcutsResponse
,
error
)
{
userID
,
err
:=
ExtractUserIDFromName
(
request
.
Parent
)
if
err
!=
nil
{
...
...
@@ -46,7 +74,7 @@ func (s *APIV1Service) ListShortcuts(ctx context.Context, request *v1pb.ListShor
shortcuts
:=
[]
*
v1pb
.
Shortcut
{}
for
_
,
shortcut
:=
range
shortcutsUserSetting
.
GetShortcuts
()
{
shortcuts
=
append
(
shortcuts
,
&
v1pb
.
Shortcut
{
Id
:
shortcut
.
GetId
(
),
Name
:
constructShortcutName
(
userID
,
shortcut
.
GetId
()
),
Title
:
shortcut
.
GetTitle
(),
Filter
:
shortcut
.
GetFilter
(),
})
...
...
@@ -57,6 +85,45 @@ func (s *APIV1Service) ListShortcuts(ctx context.Context, request *v1pb.ListShor
},
nil
}
func
(
s
*
APIV1Service
)
GetShortcut
(
ctx
context
.
Context
,
request
*
v1pb
.
GetShortcutRequest
)
(
*
v1pb
.
Shortcut
,
error
)
{
userID
,
shortcutID
,
err
:=
extractUserAndShortcutIDFromName
(
request
.
Name
)
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"invalid shortcut name: %v"
,
err
)
}
currentUser
,
err
:=
s
.
GetCurrentUser
(
ctx
)
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to get current user: %v"
,
err
)
}
if
currentUser
==
nil
||
currentUser
.
ID
!=
userID
{
return
nil
,
status
.
Errorf
(
codes
.
PermissionDenied
,
"permission denied"
)
}
userSetting
,
err
:=
s
.
Store
.
GetUserSetting
(
ctx
,
&
store
.
FindUserSetting
{
UserID
:
&
userID
,
Key
:
storepb
.
UserSettingKey_SHORTCUTS
,
})
if
err
!=
nil
{
return
nil
,
err
}
if
userSetting
==
nil
{
return
nil
,
status
.
Errorf
(
codes
.
NotFound
,
"shortcut not found"
)
}
shortcutsUserSetting
:=
userSetting
.
GetShortcuts
()
for
_
,
shortcut
:=
range
shortcutsUserSetting
.
GetShortcuts
()
{
if
shortcut
.
GetId
()
==
shortcutID
{
return
&
v1pb
.
Shortcut
{
Name
:
constructShortcutName
(
userID
,
shortcut
.
GetId
()),
Title
:
shortcut
.
GetTitle
(),
Filter
:
shortcut
.
GetFilter
(),
},
nil
}
}
return
nil
,
status
.
Errorf
(
codes
.
NotFound
,
"shortcut not found"
)
}
func
(
s
*
APIV1Service
)
CreateShortcut
(
ctx
context
.
Context
,
request
*
v1pb
.
CreateShortcutRequest
)
(
*
v1pb
.
Shortcut
,
error
)
{
userID
,
err
:=
ExtractUserIDFromName
(
request
.
Parent
)
if
err
!=
nil
{
...
...
@@ -84,7 +151,7 @@ func (s *APIV1Service) CreateShortcut(ctx context.Context, request *v1pb.CreateS
}
if
request
.
ValidateOnly
{
return
&
v1pb
.
Shortcut
{
Id
:
newShortcut
.
GetId
(
),
Name
:
constructShortcutName
(
userID
,
newShortcut
.
GetId
()
),
Title
:
newShortcut
.
GetTitle
(),
Filter
:
newShortcut
.
GetFilter
(),
},
nil
...
...
@@ -123,16 +190,16 @@ func (s *APIV1Service) CreateShortcut(ctx context.Context, request *v1pb.CreateS
}
return
&
v1pb
.
Shortcut
{
Id
:
request
.
Shortcut
.
GetId
(
),
Title
:
request
.
Shortcut
.
GetTitle
(),
Filter
:
request
.
Shortcut
.
GetFilter
(),
Name
:
constructShortcutName
(
userID
,
newShortcut
.
GetId
()
),
Title
:
new
Shortcut
.
GetTitle
(),
Filter
:
new
Shortcut
.
GetFilter
(),
},
nil
}
func
(
s
*
APIV1Service
)
UpdateShortcut
(
ctx
context
.
Context
,
request
*
v1pb
.
UpdateShortcutRequest
)
(
*
v1pb
.
Shortcut
,
error
)
{
userID
,
err
:=
ExtractUserIDFromName
(
request
.
Parent
)
userID
,
shortcutID
,
err
:=
extractUserAndShortcutIDFromName
(
request
.
Shortcut
.
Name
)
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"invalid
user
name: %v"
,
err
)
return
nil
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"invalid
shortcut
name: %v"
,
err
)
}
currentUser
,
err
:=
s
.
GetCurrentUser
(
ctx
)
...
...
@@ -159,9 +226,11 @@ func (s *APIV1Service) UpdateShortcut(ctx context.Context, request *v1pb.UpdateS
shortcutsUserSetting
:=
userSetting
.
GetShortcuts
()
shortcuts
:=
shortcutsUserSetting
.
GetShortcuts
()
var
foundShortcut
*
storepb
.
ShortcutsUserSetting_Shortcut
newShortcuts
:=
make
([]
*
storepb
.
ShortcutsUserSetting_Shortcut
,
0
,
len
(
shortcuts
))
for
_
,
shortcut
:=
range
shortcuts
{
if
shortcut
.
GetId
()
==
request
.
Shortcut
.
GetId
()
{
if
shortcut
.
GetId
()
==
shortcutID
{
foundShortcut
=
shortcut
for
_
,
field
:=
range
request
.
UpdateMask
.
Paths
{
if
field
==
"title"
{
if
request
.
Shortcut
.
GetTitle
()
==
""
{
...
...
@@ -178,6 +247,11 @@ func (s *APIV1Service) UpdateShortcut(ctx context.Context, request *v1pb.UpdateS
}
newShortcuts
=
append
(
newShortcuts
,
shortcut
)
}
if
foundShortcut
==
nil
{
return
nil
,
status
.
Errorf
(
codes
.
NotFound
,
"shortcut not found"
)
}
shortcutsUserSetting
.
Shortcuts
=
newShortcuts
userSetting
.
Value
=
&
storepb
.
UserSetting_Shortcuts
{
Shortcuts
:
shortcutsUserSetting
,
...
...
@@ -188,16 +262,16 @@ func (s *APIV1Service) UpdateShortcut(ctx context.Context, request *v1pb.UpdateS
}
return
&
v1pb
.
Shortcut
{
Id
:
request
.
Shortcut
.
GetId
(
),
Title
:
request
.
Shortcut
.
GetTitle
(),
Filter
:
request
.
Shortcut
.
GetFilter
(),
Name
:
constructShortcutName
(
userID
,
foundShortcut
.
GetId
()
),
Title
:
found
Shortcut
.
GetTitle
(),
Filter
:
found
Shortcut
.
GetFilter
(),
},
nil
}
func
(
s
*
APIV1Service
)
DeleteShortcut
(
ctx
context
.
Context
,
request
*
v1pb
.
DeleteShortcutRequest
)
(
*
emptypb
.
Empty
,
error
)
{
userID
,
err
:=
ExtractUserIDFromName
(
request
.
Parent
)
userID
,
shortcutID
,
err
:=
extractUserAndShortcutIDFromName
(
request
.
Name
)
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"invalid
user
name: %v"
,
err
)
return
nil
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"invalid
shortcut
name: %v"
,
err
)
}
currentUser
,
err
:=
s
.
GetCurrentUser
(
ctx
)
...
...
@@ -223,7 +297,7 @@ func (s *APIV1Service) DeleteShortcut(ctx context.Context, request *v1pb.DeleteS
shortcuts
:=
shortcutsUserSetting
.
GetShortcuts
()
newShortcuts
:=
make
([]
*
storepb
.
ShortcutsUserSetting_Shortcut
,
0
,
len
(
shortcuts
))
for
_
,
shortcut
:=
range
shortcuts
{
if
shortcut
.
GetId
()
!=
request
.
Id
{
if
shortcut
.
GetId
()
!=
shortcutID
{
newShortcuts
=
append
(
newShortcuts
,
shortcut
)
}
}
...
...
web/src/components/CreateShortcutDialog.tsx
View file @
8d8e9b3b
...
...
@@ -8,7 +8,6 @@ import useLoading from "@/hooks/useLoading";
import
{
userStore
}
from
"@/store/v2"
;
import
{
Shortcut
}
from
"@/types/proto/api/v1/shortcut_service"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
generateUUID
}
from
"@/utils/uuid"
;
import
{
generateDialog
}
from
"./Dialog"
;
interface
Props
extends
DialogProps
{
...
...
@@ -20,7 +19,7 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
const
t
=
useTranslate
();
const
user
=
useCurrentUser
();
const
[
shortcut
,
setShortcut
]
=
useState
<
Shortcut
>
({
id
:
props
.
shortcut
?.
id
||
""
,
name
:
props
.
shortcut
?.
name
||
""
,
title
:
props
.
shortcut
?.
title
||
""
,
filter
:
props
.
shortcut
?.
filter
||
""
,
});
...
...
@@ -46,13 +45,20 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
await
shortcutServiceClient
.
createShortcut
({
parent
:
user
.
name
,
shortcut
:
{
...
shortcut
,
id
:
generateUUID
(),
name
:
""
,
// Will be set by server
title
:
shortcut
.
title
,
filter
:
shortcut
.
filter
,
},
});
toast
.
success
(
"Create shortcut successfully"
);
}
else
{
await
shortcutServiceClient
.
updateShortcut
({
parent
:
user
.
name
,
shortcut
,
updateMask
:
[
"title"
,
"filter"
]
});
await
shortcutServiceClient
.
updateShortcut
({
shortcut
:
{
...
shortcut
,
name
:
props
.
shortcut
!
.
name
,
// Keep the original resource name
},
updateMask
:
[
"title"
,
"filter"
],
});
toast
.
success
(
"Update shortcut successfully"
);
}
// Refresh shortcuts.
...
...
web/src/components/HomeSidebar/ShortcutsSection.tsx
View file @
8d8e9b3b
...
...
@@ -3,7 +3,6 @@ import { Edit3Icon, MoreVerticalIcon, TrashIcon, PlusIcon } from "lucide-react";
import
{
observer
}
from
"mobx-react-lite"
;
import
{
shortcutServiceClient
}
from
"@/grpcweb"
;
import
useAsyncEffect
from
"@/hooks/useAsyncEffect"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
{
userStore
}
from
"@/store/v2"
;
import
memoFilterStore
from
"@/store/v2/memoFilter"
;
import
{
Shortcut
}
from
"@/types/proto/api/v1/shortcut_service"
;
...
...
@@ -14,9 +13,15 @@ import { Popover, PopoverContent, PopoverTrigger } from "../ui/Popover";
const
emojiRegex
=
/^
(\p
{Emoji_Presentation}|
\p
{Emoji}
\u
FE0F
)
$/
u
;
// Helper function to extract shortcut ID from resource name
// Format: users/{user}/shortcuts/{shortcut}
const
getShortcutId
=
(
name
:
string
):
string
=>
{
const
parts
=
name
.
split
(
"/"
);
return
parts
.
length
===
4
?
parts
[
3
]
:
""
;
};
const
ShortcutsSection
=
observer
(()
=>
{
const
t
=
useTranslate
();
const
user
=
useCurrentUser
();
const
shortcuts
=
userStore
.
state
.
shortcuts
;
useAsyncEffect
(
async
()
=>
{
...
...
@@ -26,7 +31,7 @@ const ShortcutsSection = observer(() => {
const
handleDeleteShortcut
=
async
(
shortcut
:
Shortcut
)
=>
{
const
confirmed
=
window
.
confirm
(
"Are you sure you want to delete this shortcut?"
);
if
(
confirmed
)
{
await
shortcutServiceClient
.
deleteShortcut
({
parent
:
user
.
name
,
id
:
shortcut
.
id
});
await
shortcutServiceClient
.
deleteShortcut
({
name
:
shortcut
.
name
});
await
userStore
.
fetchShortcuts
();
}
};
...
...
@@ -41,18 +46,19 @@ const ShortcutsSection = observer(() => {
</
div
>
<
div
className=
"w-full flex flex-row justify-start items-center relative flex-wrap gap-x-2 gap-y-1"
>
{
shortcuts
.
map
((
shortcut
)
=>
{
const
shortcutId
=
getShortcutId
(
shortcut
.
name
);
const
maybeEmoji
=
shortcut
.
title
.
split
(
" "
)[
0
];
const
emoji
=
emojiRegex
.
test
(
maybeEmoji
)
?
maybeEmoji
:
undefined
;
const
title
=
emoji
?
shortcut
.
title
.
replace
(
emoji
,
""
)
:
shortcut
.
title
;
const
selected
=
memoFilterStore
.
shortcut
===
shortcut
.
i
d
;
const
selected
=
memoFilterStore
.
shortcut
===
shortcut
I
d
;
return
(
<
div
key=
{
shortcut
.
i
d
}
key=
{
shortcut
I
d
}
className=
"shrink-0 w-full text-sm rounded-md leading-6 flex flex-row justify-between items-center select-none gap-2 text-gray-600 dark:text-gray-400 dark:border-zinc-800"
>
<
span
className=
{
cn
(
"truncate cursor-pointer dark:opacity-80"
,
selected
&&
"text-primary font-medium"
)
}
onClick=
{
()
=>
(
selected
?
memoFilterStore
.
setShortcut
(
undefined
)
:
memoFilterStore
.
setShortcut
(
shortcut
.
i
d
))
}
onClick=
{
()
=>
(
selected
?
memoFilterStore
.
setShortcut
(
undefined
)
:
memoFilterStore
.
setShortcut
(
shortcut
I
d
))
}
>
{
emoji
&&
<
span
className=
"text-base mr-1"
>
{
emoji
}
</
span
>
}
{
title
.
trim
()
}
...
...
web/src/pages/Home.tsx
View file @
8d8e9b3b
...
...
@@ -9,9 +9,16 @@ import memoFilterStore from "@/store/v2/memoFilter";
import
{
Direction
,
State
}
from
"@/types/proto/api/v1/common"
;
import
{
Memo
}
from
"@/types/proto/api/v1/memo_service"
;
// Helper function to extract shortcut ID from resource name
// Format: users/{user}/shortcuts/{shortcut}
const
getShortcutId
=
(
name
:
string
):
string
=>
{
const
parts
=
name
.
split
(
"/"
);
return
parts
.
length
===
4
?
parts
[
3
]
:
""
;
};
const
Home
=
observer
(()
=>
{
const
user
=
useCurrentUser
();
const
selectedShortcut
=
userStore
.
state
.
shortcuts
.
find
((
shortcut
)
=>
shortcut
.
id
===
memoFilterStore
.
shortcut
);
const
selectedShortcut
=
userStore
.
state
.
shortcuts
.
find
((
shortcut
)
=>
getShortcutId
(
shortcut
.
name
)
===
memoFilterStore
.
shortcut
);
const
memoListFilter
=
useMemo
(()
=>
{
const
conditions
=
[];
...
...
web/src/types/proto/api/v1/shortcut_service.ts
View file @
8d8e9b3b
This diff is collapsed.
Click to expand it.
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