feat(views): support custom icons for saved memo views (#6305)
This commit is contained in:
parent
e02899e514
commit
51b434d3b5
27 changed files with 1082 additions and 424 deletions
|
|
@ -598,6 +598,19 @@ message MemoView {
|
||||||
// The CEL filter expression for the memo view, using the same grammar as the
|
// The CEL filter expression for the memo view, using the same grammar as the
|
||||||
// ListMemos `filter` argument. Reuse it by passing this value to ListMemos.
|
// ListMemos `filter` argument. Reuse it by passing this value to ListMemos.
|
||||||
string filter = 3 [(google.api.field_behavior) = REQUIRED];
|
string filter = 3 [(google.api.field_behavior) = REQUIRED];
|
||||||
|
|
||||||
|
// Optional. Display icon. Omit when updating with the "icon" mask to reset
|
||||||
|
// to the default view icon.
|
||||||
|
Icon icon = 4 [(google.api.field_behavior) = OPTIONAL];
|
||||||
|
|
||||||
|
message Icon {
|
||||||
|
oneof value {
|
||||||
|
// A Unicode emoji sequence.
|
||||||
|
string emoji = 1;
|
||||||
|
// A canonical Lucide icon name, such as "leaf".
|
||||||
|
string lucide = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message ListMemoViewsRequest {
|
message ListMemoViewsRequest {
|
||||||
|
|
|
||||||
|
|
@ -1497,7 +1497,10 @@ type MemoView struct {
|
||||||
Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"`
|
Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"`
|
||||||
// The CEL filter expression for the memo view, using the same grammar as the
|
// The CEL filter expression for the memo view, using the same grammar as the
|
||||||
// ListMemos `filter` argument. Reuse it by passing this value to ListMemos.
|
// ListMemos `filter` argument. Reuse it by passing this value to ListMemos.
|
||||||
Filter string `protobuf:"bytes,3,opt,name=filter,proto3" json:"filter,omitempty"`
|
Filter string `protobuf:"bytes,3,opt,name=filter,proto3" json:"filter,omitempty"`
|
||||||
|
// Optional. Display icon. Omit when updating with the "icon" mask to reset
|
||||||
|
// to the default view icon.
|
||||||
|
Icon *MemoView_Icon `protobuf:"bytes,4,opt,name=icon,proto3" json:"icon,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
|
|
@ -1553,6 +1556,13 @@ func (x *MemoView) GetFilter() string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *MemoView) GetIcon() *MemoView_Icon {
|
||||||
|
if x != nil {
|
||||||
|
return x.Icon
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type ListMemoViewsRequest struct {
|
type ListMemoViewsRequest struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
// Required. The parent resource where memo views are listed.
|
// Required. The parent resource where memo views are listed.
|
||||||
|
|
@ -3658,6 +3668,90 @@ func (x *UserSetting_WebhooksSetting) GetWebhooks() []*UserWebhook {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MemoView_Icon struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
// Types that are valid to be assigned to Value:
|
||||||
|
//
|
||||||
|
// *MemoView_Icon_Emoji
|
||||||
|
// *MemoView_Icon_Lucide
|
||||||
|
Value isMemoView_Icon_Value `protobuf_oneof:"value"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MemoView_Icon) Reset() {
|
||||||
|
*x = MemoView_Icon{}
|
||||||
|
mi := &file_api_v1_user_service_proto_msgTypes[57]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MemoView_Icon) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*MemoView_Icon) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *MemoView_Icon) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_api_v1_user_service_proto_msgTypes[57]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use MemoView_Icon.ProtoReflect.Descriptor instead.
|
||||||
|
func (*MemoView_Icon) Descriptor() ([]byte, []int) {
|
||||||
|
return file_api_v1_user_service_proto_rawDescGZIP(), []int{18, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MemoView_Icon) GetValue() isMemoView_Icon_Value {
|
||||||
|
if x != nil {
|
||||||
|
return x.Value
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MemoView_Icon) GetEmoji() string {
|
||||||
|
if x != nil {
|
||||||
|
if x, ok := x.Value.(*MemoView_Icon_Emoji); ok {
|
||||||
|
return x.Emoji
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MemoView_Icon) GetLucide() string {
|
||||||
|
if x != nil {
|
||||||
|
if x, ok := x.Value.(*MemoView_Icon_Lucide); ok {
|
||||||
|
return x.Lucide
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type isMemoView_Icon_Value interface {
|
||||||
|
isMemoView_Icon_Value()
|
||||||
|
}
|
||||||
|
|
||||||
|
type MemoView_Icon_Emoji struct {
|
||||||
|
// A Unicode emoji sequence.
|
||||||
|
Emoji string `protobuf:"bytes,1,opt,name=emoji,proto3,oneof"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MemoView_Icon_Lucide struct {
|
||||||
|
// A canonical Lucide icon name, such as "leaf".
|
||||||
|
Lucide string `protobuf:"bytes,2,opt,name=lucide,proto3,oneof"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*MemoView_Icon_Emoji) isMemoView_Icon_Value() {}
|
||||||
|
|
||||||
|
func (*MemoView_Icon_Lucide) isMemoView_Icon_Value() {}
|
||||||
|
|
||||||
type UserNotification_MemoCommentPayload struct {
|
type UserNotification_MemoCommentPayload struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
// The memo name of comment.
|
// The memo name of comment.
|
||||||
|
|
@ -3676,7 +3770,7 @@ type UserNotification_MemoCommentPayload struct {
|
||||||
|
|
||||||
func (x *UserNotification_MemoCommentPayload) Reset() {
|
func (x *UserNotification_MemoCommentPayload) Reset() {
|
||||||
*x = UserNotification_MemoCommentPayload{}
|
*x = UserNotification_MemoCommentPayload{}
|
||||||
mi := &file_api_v1_user_service_proto_msgTypes[57]
|
mi := &file_api_v1_user_service_proto_msgTypes[58]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
|
|
@ -3688,7 +3782,7 @@ func (x *UserNotification_MemoCommentPayload) String() string {
|
||||||
func (*UserNotification_MemoCommentPayload) ProtoMessage() {}
|
func (*UserNotification_MemoCommentPayload) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *UserNotification_MemoCommentPayload) ProtoReflect() protoreflect.Message {
|
func (x *UserNotification_MemoCommentPayload) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_api_v1_user_service_proto_msgTypes[57]
|
mi := &file_api_v1_user_service_proto_msgTypes[58]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
|
@ -3750,7 +3844,7 @@ type UserNotification_MemoMentionPayload struct {
|
||||||
|
|
||||||
func (x *UserNotification_MemoMentionPayload) Reset() {
|
func (x *UserNotification_MemoMentionPayload) Reset() {
|
||||||
*x = UserNotification_MemoMentionPayload{}
|
*x = UserNotification_MemoMentionPayload{}
|
||||||
mi := &file_api_v1_user_service_proto_msgTypes[58]
|
mi := &file_api_v1_user_service_proto_msgTypes[59]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
|
|
@ -3762,7 +3856,7 @@ func (x *UserNotification_MemoMentionPayload) String() string {
|
||||||
func (*UserNotification_MemoMentionPayload) ProtoMessage() {}
|
func (*UserNotification_MemoMentionPayload) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *UserNotification_MemoMentionPayload) ProtoReflect() protoreflect.Message {
|
func (x *UserNotification_MemoMentionPayload) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_api_v1_user_service_proto_msgTypes[58]
|
mi := &file_api_v1_user_service_proto_msgTypes[59]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
|
@ -3823,7 +3917,7 @@ type UserNotification_SpaceInvitationPayload struct {
|
||||||
|
|
||||||
func (x *UserNotification_SpaceInvitationPayload) Reset() {
|
func (x *UserNotification_SpaceInvitationPayload) Reset() {
|
||||||
*x = UserNotification_SpaceInvitationPayload{}
|
*x = UserNotification_SpaceInvitationPayload{}
|
||||||
mi := &file_api_v1_user_service_proto_msgTypes[59]
|
mi := &file_api_v1_user_service_proto_msgTypes[60]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
|
|
@ -3835,7 +3929,7 @@ func (x *UserNotification_SpaceInvitationPayload) String() string {
|
||||||
func (*UserNotification_SpaceInvitationPayload) ProtoMessage() {}
|
func (*UserNotification_SpaceInvitationPayload) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *UserNotification_SpaceInvitationPayload) ProtoReflect() protoreflect.Message {
|
func (x *UserNotification_SpaceInvitationPayload) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_api_v1_user_service_proto_msgTypes[59]
|
mi := &file_api_v1_user_service_proto_msgTypes[60]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
|
@ -4012,11 +4106,16 @@ const file_api_v1_user_service_proto_rawDesc = "" +
|
||||||
"page_token\x18\x03 \x01(\tB\x03\xe0A\x01R\tpageToken\"y\n" +
|
"page_token\x18\x03 \x01(\tB\x03\xe0A\x01R\tpageToken\"y\n" +
|
||||||
"\x18ListUserSettingsResponse\x125\n" +
|
"\x18ListUserSettingsResponse\x125\n" +
|
||||||
"\bsettings\x18\x01 \x03(\v2\x19.memos.api.v1.UserSettingR\bsettings\x12&\n" +
|
"\bsettings\x18\x01 \x03(\v2\x19.memos.api.v1.UserSettingR\bsettings\x12&\n" +
|
||||||
"\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"\x9f\x01\n" +
|
"\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"\x98\x02\n" +
|
||||||
"\bMemoView\x12\x17\n" +
|
"\bMemoView\x12\x17\n" +
|
||||||
"\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12\x19\n" +
|
"\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12\x19\n" +
|
||||||
"\x05title\x18\x02 \x01(\tB\x03\xe0A\x02R\x05title\x12\x1b\n" +
|
"\x05title\x18\x02 \x01(\tB\x03\xe0A\x02R\x05title\x12\x1b\n" +
|
||||||
"\x06filter\x18\x03 \x01(\tB\x03\xe0A\x02R\x06filter:B\xeaA?\n" +
|
"\x06filter\x18\x03 \x01(\tB\x03\xe0A\x02R\x06filter\x124\n" +
|
||||||
|
"\x04icon\x18\x04 \x01(\v2\x1b.memos.api.v1.MemoView.IconB\x03\xe0A\x01R\x04icon\x1aA\n" +
|
||||||
|
"\x04Icon\x12\x16\n" +
|
||||||
|
"\x05emoji\x18\x01 \x01(\tH\x00R\x05emoji\x12\x18\n" +
|
||||||
|
"\x06lucide\x18\x02 \x01(\tH\x00R\x06lucideB\a\n" +
|
||||||
|
"\x05value:B\xeaA?\n" +
|
||||||
"\x15memos.api.v1/MemoView\x12\x19users/{user}/views/{view}*\x05views2\x04view\"M\n" +
|
"\x15memos.api.v1/MemoView\x12\x19users/{user}/views/{view}*\x05views2\x04view\"M\n" +
|
||||||
"\x14ListMemoViewsRequest\x125\n" +
|
"\x14ListMemoViewsRequest\x125\n" +
|
||||||
"\x06parent\x18\x01 \x01(\tB\x1d\xe0A\x02\xfaA\x17\x12\x15memos.api.v1/MemoViewR\x06parent\"N\n" +
|
"\x06parent\x18\x01 \x01(\tB\x1d\xe0A\x02\xfaA\x17\x12\x15memos.api.v1/MemoViewR\x06parent\"N\n" +
|
||||||
|
|
@ -4235,7 +4334,7 @@ func file_api_v1_user_service_proto_rawDescGZIP() []byte {
|
||||||
}
|
}
|
||||||
|
|
||||||
var file_api_v1_user_service_proto_enumTypes = make([]protoimpl.EnumInfo, 5)
|
var file_api_v1_user_service_proto_enumTypes = make([]protoimpl.EnumInfo, 5)
|
||||||
var file_api_v1_user_service_proto_msgTypes = make([]protoimpl.MessageInfo, 60)
|
var file_api_v1_user_service_proto_msgTypes = make([]protoimpl.MessageInfo, 61)
|
||||||
var file_api_v1_user_service_proto_goTypes = []any{
|
var file_api_v1_user_service_proto_goTypes = []any{
|
||||||
(User_Role)(0), // 0: memos.api.v1.User.Role
|
(User_Role)(0), // 0: memos.api.v1.User.Role
|
||||||
(UserSetting_Key)(0), // 1: memos.api.v1.UserSetting.Key
|
(UserSetting_Key)(0), // 1: memos.api.v1.UserSetting.Key
|
||||||
|
|
@ -4299,140 +4398,142 @@ var file_api_v1_user_service_proto_goTypes = []any{
|
||||||
(*UserSetting_TagsSetting)(nil), // 59: memos.api.v1.UserSetting.TagsSetting
|
(*UserSetting_TagsSetting)(nil), // 59: memos.api.v1.UserSetting.TagsSetting
|
||||||
(*UserSetting_WebhooksSetting)(nil), // 60: memos.api.v1.UserSetting.WebhooksSetting
|
(*UserSetting_WebhooksSetting)(nil), // 60: memos.api.v1.UserSetting.WebhooksSetting
|
||||||
nil, // 61: memos.api.v1.UserSetting.TagsSetting.TagsEntry
|
nil, // 61: memos.api.v1.UserSetting.TagsSetting.TagsEntry
|
||||||
(*UserNotification_MemoCommentPayload)(nil), // 62: memos.api.v1.UserNotification.MemoCommentPayload
|
(*MemoView_Icon)(nil), // 62: memos.api.v1.MemoView.Icon
|
||||||
(*UserNotification_MemoMentionPayload)(nil), // 63: memos.api.v1.UserNotification.MemoMentionPayload
|
(*UserNotification_MemoCommentPayload)(nil), // 63: memos.api.v1.UserNotification.MemoCommentPayload
|
||||||
(*UserNotification_SpaceInvitationPayload)(nil), // 64: memos.api.v1.UserNotification.SpaceInvitationPayload
|
(*UserNotification_MemoMentionPayload)(nil), // 64: memos.api.v1.UserNotification.MemoMentionPayload
|
||||||
(State)(0), // 65: memos.api.v1.State
|
(*UserNotification_SpaceInvitationPayload)(nil), // 65: memos.api.v1.UserNotification.SpaceInvitationPayload
|
||||||
(*timestamppb.Timestamp)(nil), // 66: google.protobuf.Timestamp
|
(State)(0), // 66: memos.api.v1.State
|
||||||
(*fieldmaskpb.FieldMask)(nil), // 67: google.protobuf.FieldMask
|
(*timestamppb.Timestamp)(nil), // 67: google.protobuf.Timestamp
|
||||||
(*color.Color)(nil), // 68: google.type.Color
|
(*fieldmaskpb.FieldMask)(nil), // 68: google.protobuf.FieldMask
|
||||||
(*Space)(nil), // 69: memos.api.v1.Space
|
(*color.Color)(nil), // 69: google.type.Color
|
||||||
(SpaceMember_Role)(0), // 70: memos.api.v1.SpaceMember.Role
|
(*Space)(nil), // 70: memos.api.v1.Space
|
||||||
(*emptypb.Empty)(nil), // 71: google.protobuf.Empty
|
(SpaceMember_Role)(0), // 71: memos.api.v1.SpaceMember.Role
|
||||||
|
(*emptypb.Empty)(nil), // 72: google.protobuf.Empty
|
||||||
}
|
}
|
||||||
var file_api_v1_user_service_proto_depIdxs = []int32{
|
var file_api_v1_user_service_proto_depIdxs = []int32{
|
||||||
0, // 0: memos.api.v1.User.role:type_name -> memos.api.v1.User.Role
|
0, // 0: memos.api.v1.User.role:type_name -> memos.api.v1.User.Role
|
||||||
65, // 1: memos.api.v1.User.state:type_name -> memos.api.v1.State
|
66, // 1: memos.api.v1.User.state:type_name -> memos.api.v1.State
|
||||||
66, // 2: memos.api.v1.User.create_time:type_name -> google.protobuf.Timestamp
|
67, // 2: memos.api.v1.User.create_time:type_name -> google.protobuf.Timestamp
|
||||||
66, // 3: memos.api.v1.User.update_time:type_name -> google.protobuf.Timestamp
|
67, // 3: memos.api.v1.User.update_time:type_name -> google.protobuf.Timestamp
|
||||||
5, // 4: memos.api.v1.ListUsersResponse.users:type_name -> memos.api.v1.User
|
5, // 4: memos.api.v1.ListUsersResponse.users:type_name -> memos.api.v1.User
|
||||||
5, // 5: memos.api.v1.BatchGetUsersResponse.users:type_name -> memos.api.v1.User
|
5, // 5: memos.api.v1.BatchGetUsersResponse.users:type_name -> memos.api.v1.User
|
||||||
67, // 6: memos.api.v1.GetUserRequest.read_mask:type_name -> google.protobuf.FieldMask
|
68, // 6: memos.api.v1.GetUserRequest.read_mask:type_name -> google.protobuf.FieldMask
|
||||||
5, // 7: memos.api.v1.CreateUserRequest.user:type_name -> memos.api.v1.User
|
5, // 7: memos.api.v1.CreateUserRequest.user:type_name -> memos.api.v1.User
|
||||||
5, // 8: memos.api.v1.UpdateUserRequest.user:type_name -> memos.api.v1.User
|
5, // 8: memos.api.v1.UpdateUserRequest.user:type_name -> memos.api.v1.User
|
||||||
67, // 9: memos.api.v1.UpdateUserRequest.update_mask:type_name -> google.protobuf.FieldMask
|
68, // 9: memos.api.v1.UpdateUserRequest.update_mask:type_name -> google.protobuf.FieldMask
|
||||||
56, // 10: memos.api.v1.UserStats.memo_type_stats:type_name -> memos.api.v1.UserStats.MemoTypeStats
|
56, // 10: memos.api.v1.UserStats.memo_type_stats:type_name -> memos.api.v1.UserStats.MemoTypeStats
|
||||||
55, // 11: memos.api.v1.UserStats.tag_count:type_name -> memos.api.v1.UserStats.TagCountEntry
|
55, // 11: memos.api.v1.UserStats.tag_count:type_name -> memos.api.v1.UserStats.TagCountEntry
|
||||||
66, // 12: memos.api.v1.UserStats.memo_created_timestamps:type_name -> google.protobuf.Timestamp
|
67, // 12: memos.api.v1.UserStats.memo_created_timestamps:type_name -> google.protobuf.Timestamp
|
||||||
66, // 13: memos.api.v1.UserStats.memo_updated_timestamps:type_name -> google.protobuf.Timestamp
|
67, // 13: memos.api.v1.UserStats.memo_updated_timestamps:type_name -> google.protobuf.Timestamp
|
||||||
65, // 14: memos.api.v1.ListAllUserStatsRequest.state:type_name -> memos.api.v1.State
|
66, // 14: memos.api.v1.ListAllUserStatsRequest.state:type_name -> memos.api.v1.State
|
||||||
14, // 15: memos.api.v1.ListAllUserStatsResponse.stats:type_name -> memos.api.v1.UserStats
|
14, // 15: memos.api.v1.ListAllUserStatsResponse.stats:type_name -> memos.api.v1.UserStats
|
||||||
57, // 16: memos.api.v1.UserSetting.general_setting:type_name -> memos.api.v1.UserSetting.GeneralSetting
|
57, // 16: memos.api.v1.UserSetting.general_setting:type_name -> memos.api.v1.UserSetting.GeneralSetting
|
||||||
60, // 17: memos.api.v1.UserSetting.webhooks_setting:type_name -> memos.api.v1.UserSetting.WebhooksSetting
|
60, // 17: memos.api.v1.UserSetting.webhooks_setting:type_name -> memos.api.v1.UserSetting.WebhooksSetting
|
||||||
59, // 18: memos.api.v1.UserSetting.tags_setting:type_name -> memos.api.v1.UserSetting.TagsSetting
|
59, // 18: memos.api.v1.UserSetting.tags_setting:type_name -> memos.api.v1.UserSetting.TagsSetting
|
||||||
18, // 19: memos.api.v1.UpdateUserSettingRequest.setting:type_name -> memos.api.v1.UserSetting
|
18, // 19: memos.api.v1.UpdateUserSettingRequest.setting:type_name -> memos.api.v1.UserSetting
|
||||||
67, // 20: memos.api.v1.UpdateUserSettingRequest.update_mask:type_name -> google.protobuf.FieldMask
|
68, // 20: memos.api.v1.UpdateUserSettingRequest.update_mask:type_name -> google.protobuf.FieldMask
|
||||||
18, // 21: memos.api.v1.ListUserSettingsResponse.settings:type_name -> memos.api.v1.UserSetting
|
18, // 21: memos.api.v1.ListUserSettingsResponse.settings:type_name -> memos.api.v1.UserSetting
|
||||||
23, // 22: memos.api.v1.ListMemoViewsResponse.memo_views:type_name -> memos.api.v1.MemoView
|
62, // 22: memos.api.v1.MemoView.icon:type_name -> memos.api.v1.MemoView.Icon
|
||||||
23, // 23: memos.api.v1.CreateMemoViewRequest.memo_view:type_name -> memos.api.v1.MemoView
|
23, // 23: memos.api.v1.ListMemoViewsResponse.memo_views:type_name -> memos.api.v1.MemoView
|
||||||
23, // 24: memos.api.v1.UpdateMemoViewRequest.memo_view:type_name -> memos.api.v1.MemoView
|
23, // 24: memos.api.v1.CreateMemoViewRequest.memo_view:type_name -> memos.api.v1.MemoView
|
||||||
67, // 25: memos.api.v1.UpdateMemoViewRequest.update_mask:type_name -> google.protobuf.FieldMask
|
23, // 25: memos.api.v1.UpdateMemoViewRequest.memo_view:type_name -> memos.api.v1.MemoView
|
||||||
30, // 26: memos.api.v1.ListLinkedIdentitiesResponse.linked_identities:type_name -> memos.api.v1.LinkedIdentity
|
68, // 26: memos.api.v1.UpdateMemoViewRequest.update_mask:type_name -> google.protobuf.FieldMask
|
||||||
66, // 27: memos.api.v1.PersonalAccessToken.created_at:type_name -> google.protobuf.Timestamp
|
30, // 27: memos.api.v1.ListLinkedIdentitiesResponse.linked_identities:type_name -> memos.api.v1.LinkedIdentity
|
||||||
66, // 28: memos.api.v1.PersonalAccessToken.expires_at:type_name -> google.protobuf.Timestamp
|
67, // 28: memos.api.v1.PersonalAccessToken.created_at:type_name -> google.protobuf.Timestamp
|
||||||
66, // 29: memos.api.v1.PersonalAccessToken.last_used_at:type_name -> google.protobuf.Timestamp
|
67, // 29: memos.api.v1.PersonalAccessToken.expires_at:type_name -> google.protobuf.Timestamp
|
||||||
36, // 30: memos.api.v1.ListPersonalAccessTokensResponse.personal_access_tokens:type_name -> memos.api.v1.PersonalAccessToken
|
67, // 30: memos.api.v1.PersonalAccessToken.last_used_at:type_name -> google.protobuf.Timestamp
|
||||||
36, // 31: memos.api.v1.CreatePersonalAccessTokenResponse.personal_access_token:type_name -> memos.api.v1.PersonalAccessToken
|
36, // 31: memos.api.v1.ListPersonalAccessTokensResponse.personal_access_tokens:type_name -> memos.api.v1.PersonalAccessToken
|
||||||
66, // 32: memos.api.v1.UserWebhook.create_time:type_name -> google.protobuf.Timestamp
|
36, // 32: memos.api.v1.CreatePersonalAccessTokenResponse.personal_access_token:type_name -> memos.api.v1.PersonalAccessToken
|
||||||
66, // 33: memos.api.v1.UserWebhook.update_time:type_name -> google.protobuf.Timestamp
|
67, // 33: memos.api.v1.UserWebhook.create_time:type_name -> google.protobuf.Timestamp
|
||||||
42, // 34: memos.api.v1.ListUserWebhooksResponse.webhooks:type_name -> memos.api.v1.UserWebhook
|
67, // 34: memos.api.v1.UserWebhook.update_time:type_name -> google.protobuf.Timestamp
|
||||||
42, // 35: memos.api.v1.CreateUserWebhookRequest.webhook:type_name -> memos.api.v1.UserWebhook
|
42, // 35: memos.api.v1.ListUserWebhooksResponse.webhooks:type_name -> memos.api.v1.UserWebhook
|
||||||
42, // 36: memos.api.v1.UpdateUserWebhookRequest.webhook:type_name -> memos.api.v1.UserWebhook
|
42, // 36: memos.api.v1.CreateUserWebhookRequest.webhook:type_name -> memos.api.v1.UserWebhook
|
||||||
67, // 37: memos.api.v1.UpdateUserWebhookRequest.update_mask:type_name -> google.protobuf.FieldMask
|
42, // 37: memos.api.v1.UpdateUserWebhookRequest.webhook:type_name -> memos.api.v1.UserWebhook
|
||||||
5, // 38: memos.api.v1.UserNotification.sender_user:type_name -> memos.api.v1.User
|
68, // 38: memos.api.v1.UpdateUserWebhookRequest.update_mask:type_name -> google.protobuf.FieldMask
|
||||||
2, // 39: memos.api.v1.UserNotification.status:type_name -> memos.api.v1.UserNotification.Status
|
5, // 39: memos.api.v1.UserNotification.sender_user:type_name -> memos.api.v1.User
|
||||||
66, // 40: memos.api.v1.UserNotification.create_time:type_name -> google.protobuf.Timestamp
|
2, // 40: memos.api.v1.UserNotification.status:type_name -> memos.api.v1.UserNotification.Status
|
||||||
3, // 41: memos.api.v1.UserNotification.type:type_name -> memos.api.v1.UserNotification.Type
|
67, // 41: memos.api.v1.UserNotification.create_time:type_name -> google.protobuf.Timestamp
|
||||||
62, // 42: memos.api.v1.UserNotification.memo_comment:type_name -> memos.api.v1.UserNotification.MemoCommentPayload
|
3, // 42: memos.api.v1.UserNotification.type:type_name -> memos.api.v1.UserNotification.Type
|
||||||
63, // 43: memos.api.v1.UserNotification.memo_mention:type_name -> memos.api.v1.UserNotification.MemoMentionPayload
|
63, // 43: memos.api.v1.UserNotification.memo_comment:type_name -> memos.api.v1.UserNotification.MemoCommentPayload
|
||||||
64, // 44: memos.api.v1.UserNotification.space_invitation:type_name -> memos.api.v1.UserNotification.SpaceInvitationPayload
|
64, // 44: memos.api.v1.UserNotification.memo_mention:type_name -> memos.api.v1.UserNotification.MemoMentionPayload
|
||||||
50, // 45: memos.api.v1.ListUserNotificationsResponse.notifications:type_name -> memos.api.v1.UserNotification
|
65, // 45: memos.api.v1.UserNotification.space_invitation:type_name -> memos.api.v1.UserNotification.SpaceInvitationPayload
|
||||||
50, // 46: memos.api.v1.UpdateUserNotificationRequest.notification:type_name -> memos.api.v1.UserNotification
|
50, // 46: memos.api.v1.ListUserNotificationsResponse.notifications:type_name -> memos.api.v1.UserNotification
|
||||||
67, // 47: memos.api.v1.UpdateUserNotificationRequest.update_mask:type_name -> google.protobuf.FieldMask
|
50, // 47: memos.api.v1.UpdateUserNotificationRequest.notification:type_name -> memos.api.v1.UserNotification
|
||||||
68, // 48: memos.api.v1.UserSetting.TagMetadata.background_color:type_name -> google.type.Color
|
68, // 48: memos.api.v1.UpdateUserNotificationRequest.update_mask:type_name -> google.protobuf.FieldMask
|
||||||
61, // 49: memos.api.v1.UserSetting.TagsSetting.tags:type_name -> memos.api.v1.UserSetting.TagsSetting.TagsEntry
|
69, // 49: memos.api.v1.UserSetting.TagMetadata.background_color:type_name -> google.type.Color
|
||||||
42, // 50: memos.api.v1.UserSetting.WebhooksSetting.webhooks:type_name -> memos.api.v1.UserWebhook
|
61, // 50: memos.api.v1.UserSetting.TagsSetting.tags:type_name -> memos.api.v1.UserSetting.TagsSetting.TagsEntry
|
||||||
58, // 51: memos.api.v1.UserSetting.TagsSetting.TagsEntry.value:type_name -> memos.api.v1.UserSetting.TagMetadata
|
42, // 51: memos.api.v1.UserSetting.WebhooksSetting.webhooks:type_name -> memos.api.v1.UserWebhook
|
||||||
69, // 52: memos.api.v1.UserNotification.SpaceInvitationPayload.space:type_name -> memos.api.v1.Space
|
58, // 52: memos.api.v1.UserSetting.TagsSetting.TagsEntry.value:type_name -> memos.api.v1.UserSetting.TagMetadata
|
||||||
70, // 53: memos.api.v1.UserNotification.SpaceInvitationPayload.role:type_name -> memos.api.v1.SpaceMember.Role
|
70, // 53: memos.api.v1.UserNotification.SpaceInvitationPayload.space:type_name -> memos.api.v1.Space
|
||||||
4, // 54: memos.api.v1.UserNotification.SpaceInvitationPayload.state:type_name -> memos.api.v1.UserNotification.SpaceInvitationPayload.State
|
71, // 54: memos.api.v1.UserNotification.SpaceInvitationPayload.role:type_name -> memos.api.v1.SpaceMember.Role
|
||||||
6, // 55: memos.api.v1.UserService.ListUsers:input_type -> memos.api.v1.ListUsersRequest
|
4, // 55: memos.api.v1.UserNotification.SpaceInvitationPayload.state:type_name -> memos.api.v1.UserNotification.SpaceInvitationPayload.State
|
||||||
8, // 56: memos.api.v1.UserService.BatchGetUsers:input_type -> memos.api.v1.BatchGetUsersRequest
|
6, // 56: memos.api.v1.UserService.ListUsers:input_type -> memos.api.v1.ListUsersRequest
|
||||||
10, // 57: memos.api.v1.UserService.GetUser:input_type -> memos.api.v1.GetUserRequest
|
8, // 57: memos.api.v1.UserService.BatchGetUsers:input_type -> memos.api.v1.BatchGetUsersRequest
|
||||||
11, // 58: memos.api.v1.UserService.CreateUser:input_type -> memos.api.v1.CreateUserRequest
|
10, // 58: memos.api.v1.UserService.GetUser:input_type -> memos.api.v1.GetUserRequest
|
||||||
12, // 59: memos.api.v1.UserService.UpdateUser:input_type -> memos.api.v1.UpdateUserRequest
|
11, // 59: memos.api.v1.UserService.CreateUser:input_type -> memos.api.v1.CreateUserRequest
|
||||||
13, // 60: memos.api.v1.UserService.DeleteUser:input_type -> memos.api.v1.DeleteUserRequest
|
12, // 60: memos.api.v1.UserService.UpdateUser:input_type -> memos.api.v1.UpdateUserRequest
|
||||||
16, // 61: memos.api.v1.UserService.ListAllUserStats:input_type -> memos.api.v1.ListAllUserStatsRequest
|
13, // 61: memos.api.v1.UserService.DeleteUser:input_type -> memos.api.v1.DeleteUserRequest
|
||||||
15, // 62: memos.api.v1.UserService.GetUserStats:input_type -> memos.api.v1.GetUserStatsRequest
|
16, // 62: memos.api.v1.UserService.ListAllUserStats:input_type -> memos.api.v1.ListAllUserStatsRequest
|
||||||
19, // 63: memos.api.v1.UserService.GetUserSetting:input_type -> memos.api.v1.GetUserSettingRequest
|
15, // 63: memos.api.v1.UserService.GetUserStats:input_type -> memos.api.v1.GetUserStatsRequest
|
||||||
20, // 64: memos.api.v1.UserService.UpdateUserSetting:input_type -> memos.api.v1.UpdateUserSettingRequest
|
19, // 64: memos.api.v1.UserService.GetUserSetting:input_type -> memos.api.v1.GetUserSettingRequest
|
||||||
21, // 65: memos.api.v1.UserService.ListUserSettings:input_type -> memos.api.v1.ListUserSettingsRequest
|
20, // 65: memos.api.v1.UserService.UpdateUserSetting:input_type -> memos.api.v1.UpdateUserSettingRequest
|
||||||
24, // 66: memos.api.v1.UserService.ListMemoViews:input_type -> memos.api.v1.ListMemoViewsRequest
|
21, // 66: memos.api.v1.UserService.ListUserSettings:input_type -> memos.api.v1.ListUserSettingsRequest
|
||||||
26, // 67: memos.api.v1.UserService.GetMemoView:input_type -> memos.api.v1.GetMemoViewRequest
|
24, // 67: memos.api.v1.UserService.ListMemoViews:input_type -> memos.api.v1.ListMemoViewsRequest
|
||||||
27, // 68: memos.api.v1.UserService.CreateMemoView:input_type -> memos.api.v1.CreateMemoViewRequest
|
26, // 68: memos.api.v1.UserService.GetMemoView:input_type -> memos.api.v1.GetMemoViewRequest
|
||||||
28, // 69: memos.api.v1.UserService.UpdateMemoView:input_type -> memos.api.v1.UpdateMemoViewRequest
|
27, // 69: memos.api.v1.UserService.CreateMemoView:input_type -> memos.api.v1.CreateMemoViewRequest
|
||||||
29, // 70: memos.api.v1.UserService.DeleteMemoView:input_type -> memos.api.v1.DeleteMemoViewRequest
|
28, // 70: memos.api.v1.UserService.UpdateMemoView:input_type -> memos.api.v1.UpdateMemoViewRequest
|
||||||
31, // 71: memos.api.v1.UserService.ListLinkedIdentities:input_type -> memos.api.v1.ListLinkedIdentitiesRequest
|
29, // 71: memos.api.v1.UserService.DeleteMemoView:input_type -> memos.api.v1.DeleteMemoViewRequest
|
||||||
33, // 72: memos.api.v1.UserService.CreateLinkedIdentity:input_type -> memos.api.v1.CreateLinkedIdentityRequest
|
31, // 72: memos.api.v1.UserService.ListLinkedIdentities:input_type -> memos.api.v1.ListLinkedIdentitiesRequest
|
||||||
34, // 73: memos.api.v1.UserService.GetLinkedIdentity:input_type -> memos.api.v1.GetLinkedIdentityRequest
|
33, // 73: memos.api.v1.UserService.CreateLinkedIdentity:input_type -> memos.api.v1.CreateLinkedIdentityRequest
|
||||||
35, // 74: memos.api.v1.UserService.DeleteLinkedIdentity:input_type -> memos.api.v1.DeleteLinkedIdentityRequest
|
34, // 74: memos.api.v1.UserService.GetLinkedIdentity:input_type -> memos.api.v1.GetLinkedIdentityRequest
|
||||||
37, // 75: memos.api.v1.UserService.ListPersonalAccessTokens:input_type -> memos.api.v1.ListPersonalAccessTokensRequest
|
35, // 75: memos.api.v1.UserService.DeleteLinkedIdentity:input_type -> memos.api.v1.DeleteLinkedIdentityRequest
|
||||||
39, // 76: memos.api.v1.UserService.CreatePersonalAccessToken:input_type -> memos.api.v1.CreatePersonalAccessTokenRequest
|
37, // 76: memos.api.v1.UserService.ListPersonalAccessTokens:input_type -> memos.api.v1.ListPersonalAccessTokensRequest
|
||||||
41, // 77: memos.api.v1.UserService.DeletePersonalAccessToken:input_type -> memos.api.v1.DeletePersonalAccessTokenRequest
|
39, // 77: memos.api.v1.UserService.CreatePersonalAccessToken:input_type -> memos.api.v1.CreatePersonalAccessTokenRequest
|
||||||
43, // 78: memos.api.v1.UserService.ListUserWebhooks:input_type -> memos.api.v1.ListUserWebhooksRequest
|
41, // 78: memos.api.v1.UserService.DeletePersonalAccessToken:input_type -> memos.api.v1.DeletePersonalAccessTokenRequest
|
||||||
45, // 79: memos.api.v1.UserService.CreateUserWebhook:input_type -> memos.api.v1.CreateUserWebhookRequest
|
43, // 79: memos.api.v1.UserService.ListUserWebhooks:input_type -> memos.api.v1.ListUserWebhooksRequest
|
||||||
46, // 80: memos.api.v1.UserService.UpdateUserWebhook:input_type -> memos.api.v1.UpdateUserWebhookRequest
|
45, // 80: memos.api.v1.UserService.CreateUserWebhook:input_type -> memos.api.v1.CreateUserWebhookRequest
|
||||||
47, // 81: memos.api.v1.UserService.DeleteUserWebhook:input_type -> memos.api.v1.DeleteUserWebhookRequest
|
46, // 81: memos.api.v1.UserService.UpdateUserWebhook:input_type -> memos.api.v1.UpdateUserWebhookRequest
|
||||||
48, // 82: memos.api.v1.UserService.GetUserWebhookSigningSecret:input_type -> memos.api.v1.GetUserWebhookSigningSecretRequest
|
47, // 82: memos.api.v1.UserService.DeleteUserWebhook:input_type -> memos.api.v1.DeleteUserWebhookRequest
|
||||||
51, // 83: memos.api.v1.UserService.ListUserNotifications:input_type -> memos.api.v1.ListUserNotificationsRequest
|
48, // 83: memos.api.v1.UserService.GetUserWebhookSigningSecret:input_type -> memos.api.v1.GetUserWebhookSigningSecretRequest
|
||||||
53, // 84: memos.api.v1.UserService.UpdateUserNotification:input_type -> memos.api.v1.UpdateUserNotificationRequest
|
51, // 84: memos.api.v1.UserService.ListUserNotifications:input_type -> memos.api.v1.ListUserNotificationsRequest
|
||||||
54, // 85: memos.api.v1.UserService.DeleteUserNotification:input_type -> memos.api.v1.DeleteUserNotificationRequest
|
53, // 85: memos.api.v1.UserService.UpdateUserNotification:input_type -> memos.api.v1.UpdateUserNotificationRequest
|
||||||
7, // 86: memos.api.v1.UserService.ListUsers:output_type -> memos.api.v1.ListUsersResponse
|
54, // 86: memos.api.v1.UserService.DeleteUserNotification:input_type -> memos.api.v1.DeleteUserNotificationRequest
|
||||||
9, // 87: memos.api.v1.UserService.BatchGetUsers:output_type -> memos.api.v1.BatchGetUsersResponse
|
7, // 87: memos.api.v1.UserService.ListUsers:output_type -> memos.api.v1.ListUsersResponse
|
||||||
5, // 88: memos.api.v1.UserService.GetUser:output_type -> memos.api.v1.User
|
9, // 88: memos.api.v1.UserService.BatchGetUsers:output_type -> memos.api.v1.BatchGetUsersResponse
|
||||||
5, // 89: memos.api.v1.UserService.CreateUser:output_type -> memos.api.v1.User
|
5, // 89: memos.api.v1.UserService.GetUser:output_type -> memos.api.v1.User
|
||||||
5, // 90: memos.api.v1.UserService.UpdateUser:output_type -> memos.api.v1.User
|
5, // 90: memos.api.v1.UserService.CreateUser:output_type -> memos.api.v1.User
|
||||||
71, // 91: memos.api.v1.UserService.DeleteUser:output_type -> google.protobuf.Empty
|
5, // 91: memos.api.v1.UserService.UpdateUser:output_type -> memos.api.v1.User
|
||||||
17, // 92: memos.api.v1.UserService.ListAllUserStats:output_type -> memos.api.v1.ListAllUserStatsResponse
|
72, // 92: memos.api.v1.UserService.DeleteUser:output_type -> google.protobuf.Empty
|
||||||
14, // 93: memos.api.v1.UserService.GetUserStats:output_type -> memos.api.v1.UserStats
|
17, // 93: memos.api.v1.UserService.ListAllUserStats:output_type -> memos.api.v1.ListAllUserStatsResponse
|
||||||
18, // 94: memos.api.v1.UserService.GetUserSetting:output_type -> memos.api.v1.UserSetting
|
14, // 94: memos.api.v1.UserService.GetUserStats:output_type -> memos.api.v1.UserStats
|
||||||
18, // 95: memos.api.v1.UserService.UpdateUserSetting:output_type -> memos.api.v1.UserSetting
|
18, // 95: memos.api.v1.UserService.GetUserSetting:output_type -> memos.api.v1.UserSetting
|
||||||
22, // 96: memos.api.v1.UserService.ListUserSettings:output_type -> memos.api.v1.ListUserSettingsResponse
|
18, // 96: memos.api.v1.UserService.UpdateUserSetting:output_type -> memos.api.v1.UserSetting
|
||||||
25, // 97: memos.api.v1.UserService.ListMemoViews:output_type -> memos.api.v1.ListMemoViewsResponse
|
22, // 97: memos.api.v1.UserService.ListUserSettings:output_type -> memos.api.v1.ListUserSettingsResponse
|
||||||
23, // 98: memos.api.v1.UserService.GetMemoView:output_type -> memos.api.v1.MemoView
|
25, // 98: memos.api.v1.UserService.ListMemoViews:output_type -> memos.api.v1.ListMemoViewsResponse
|
||||||
23, // 99: memos.api.v1.UserService.CreateMemoView:output_type -> memos.api.v1.MemoView
|
23, // 99: memos.api.v1.UserService.GetMemoView:output_type -> memos.api.v1.MemoView
|
||||||
23, // 100: memos.api.v1.UserService.UpdateMemoView:output_type -> memos.api.v1.MemoView
|
23, // 100: memos.api.v1.UserService.CreateMemoView:output_type -> memos.api.v1.MemoView
|
||||||
71, // 101: memos.api.v1.UserService.DeleteMemoView:output_type -> google.protobuf.Empty
|
23, // 101: memos.api.v1.UserService.UpdateMemoView:output_type -> memos.api.v1.MemoView
|
||||||
32, // 102: memos.api.v1.UserService.ListLinkedIdentities:output_type -> memos.api.v1.ListLinkedIdentitiesResponse
|
72, // 102: memos.api.v1.UserService.DeleteMemoView:output_type -> google.protobuf.Empty
|
||||||
30, // 103: memos.api.v1.UserService.CreateLinkedIdentity:output_type -> memos.api.v1.LinkedIdentity
|
32, // 103: memos.api.v1.UserService.ListLinkedIdentities:output_type -> memos.api.v1.ListLinkedIdentitiesResponse
|
||||||
30, // 104: memos.api.v1.UserService.GetLinkedIdentity:output_type -> memos.api.v1.LinkedIdentity
|
30, // 104: memos.api.v1.UserService.CreateLinkedIdentity:output_type -> memos.api.v1.LinkedIdentity
|
||||||
71, // 105: memos.api.v1.UserService.DeleteLinkedIdentity:output_type -> google.protobuf.Empty
|
30, // 105: memos.api.v1.UserService.GetLinkedIdentity:output_type -> memos.api.v1.LinkedIdentity
|
||||||
38, // 106: memos.api.v1.UserService.ListPersonalAccessTokens:output_type -> memos.api.v1.ListPersonalAccessTokensResponse
|
72, // 106: memos.api.v1.UserService.DeleteLinkedIdentity:output_type -> google.protobuf.Empty
|
||||||
40, // 107: memos.api.v1.UserService.CreatePersonalAccessToken:output_type -> memos.api.v1.CreatePersonalAccessTokenResponse
|
38, // 107: memos.api.v1.UserService.ListPersonalAccessTokens:output_type -> memos.api.v1.ListPersonalAccessTokensResponse
|
||||||
71, // 108: memos.api.v1.UserService.DeletePersonalAccessToken:output_type -> google.protobuf.Empty
|
40, // 108: memos.api.v1.UserService.CreatePersonalAccessToken:output_type -> memos.api.v1.CreatePersonalAccessTokenResponse
|
||||||
44, // 109: memos.api.v1.UserService.ListUserWebhooks:output_type -> memos.api.v1.ListUserWebhooksResponse
|
72, // 109: memos.api.v1.UserService.DeletePersonalAccessToken:output_type -> google.protobuf.Empty
|
||||||
42, // 110: memos.api.v1.UserService.CreateUserWebhook:output_type -> memos.api.v1.UserWebhook
|
44, // 110: memos.api.v1.UserService.ListUserWebhooks:output_type -> memos.api.v1.ListUserWebhooksResponse
|
||||||
42, // 111: memos.api.v1.UserService.UpdateUserWebhook:output_type -> memos.api.v1.UserWebhook
|
42, // 111: memos.api.v1.UserService.CreateUserWebhook:output_type -> memos.api.v1.UserWebhook
|
||||||
71, // 112: memos.api.v1.UserService.DeleteUserWebhook:output_type -> google.protobuf.Empty
|
42, // 112: memos.api.v1.UserService.UpdateUserWebhook:output_type -> memos.api.v1.UserWebhook
|
||||||
49, // 113: memos.api.v1.UserService.GetUserWebhookSigningSecret:output_type -> memos.api.v1.GetUserWebhookSigningSecretResponse
|
72, // 113: memos.api.v1.UserService.DeleteUserWebhook:output_type -> google.protobuf.Empty
|
||||||
52, // 114: memos.api.v1.UserService.ListUserNotifications:output_type -> memos.api.v1.ListUserNotificationsResponse
|
49, // 114: memos.api.v1.UserService.GetUserWebhookSigningSecret:output_type -> memos.api.v1.GetUserWebhookSigningSecretResponse
|
||||||
50, // 115: memos.api.v1.UserService.UpdateUserNotification:output_type -> memos.api.v1.UserNotification
|
52, // 115: memos.api.v1.UserService.ListUserNotifications:output_type -> memos.api.v1.ListUserNotificationsResponse
|
||||||
71, // 116: memos.api.v1.UserService.DeleteUserNotification:output_type -> google.protobuf.Empty
|
50, // 116: memos.api.v1.UserService.UpdateUserNotification:output_type -> memos.api.v1.UserNotification
|
||||||
86, // [86:117] is the sub-list for method output_type
|
72, // 117: memos.api.v1.UserService.DeleteUserNotification:output_type -> google.protobuf.Empty
|
||||||
55, // [55:86] is the sub-list for method input_type
|
87, // [87:118] is the sub-list for method output_type
|
||||||
55, // [55:55] is the sub-list for extension type_name
|
56, // [56:87] is the sub-list for method input_type
|
||||||
55, // [55:55] is the sub-list for extension extendee
|
56, // [56:56] is the sub-list for extension type_name
|
||||||
0, // [0:55] is the sub-list for field type_name
|
56, // [56:56] is the sub-list for extension extendee
|
||||||
|
0, // [0:56] is the sub-list for field type_name
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() { file_api_v1_user_service_proto_init() }
|
func init() { file_api_v1_user_service_proto_init() }
|
||||||
|
|
@ -4452,13 +4553,17 @@ func file_api_v1_user_service_proto_init() {
|
||||||
(*UserNotification_MemoMention)(nil),
|
(*UserNotification_MemoMention)(nil),
|
||||||
(*UserNotification_SpaceInvitation)(nil),
|
(*UserNotification_SpaceInvitation)(nil),
|
||||||
}
|
}
|
||||||
|
file_api_v1_user_service_proto_msgTypes[57].OneofWrappers = []any{
|
||||||
|
(*MemoView_Icon_Emoji)(nil),
|
||||||
|
(*MemoView_Icon_Lucide)(nil),
|
||||||
|
}
|
||||||
type x struct{}
|
type x struct{}
|
||||||
out := protoimpl.TypeBuilder{
|
out := protoimpl.TypeBuilder{
|
||||||
File: protoimpl.DescBuilder{
|
File: protoimpl.DescBuilder{
|
||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_v1_user_service_proto_rawDesc), len(file_api_v1_user_service_proto_rawDesc)),
|
RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_v1_user_service_proto_rawDesc), len(file_api_v1_user_service_proto_rawDesc)),
|
||||||
NumEnums: 5,
|
NumEnums: 5,
|
||||||
NumMessages: 60,
|
NumMessages: 61,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 1,
|
NumServices: 1,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4119,7 +4119,22 @@ components:
|
||||||
description: |-
|
description: |-
|
||||||
The CEL filter expression for the memo view, using the same grammar as the
|
The CEL filter expression for the memo view, using the same grammar as the
|
||||||
ListMemos `filter` argument. Reuse it by passing this value to ListMemos.
|
ListMemos `filter` argument. Reuse it by passing this value to ListMemos.
|
||||||
|
icon:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/components/schemas/MemoView_Icon'
|
||||||
|
description: |-
|
||||||
|
Optional. Display icon. Omit when updating with the "icon" mask to reset
|
||||||
|
to the default view icon.
|
||||||
description: Saved memo view messages
|
description: Saved memo view messages
|
||||||
|
MemoView_Icon:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
emoji:
|
||||||
|
type: string
|
||||||
|
description: A Unicode emoji sequence.
|
||||||
|
lucide:
|
||||||
|
type: string
|
||||||
|
description: A canonical Lucide icon name, such as "leaf".
|
||||||
Memo_Property:
|
Memo_Property:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|
|
||||||
|
|
@ -857,10 +857,11 @@ func (x *PersonalAccessTokensUserSetting_PersonalAccessToken) GetLastUsedAt() *t
|
||||||
}
|
}
|
||||||
|
|
||||||
type MemoViewsUserSetting_MemoView struct {
|
type MemoViewsUserSetting_MemoView struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||||
Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"`
|
Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"`
|
||||||
Filter string `protobuf:"bytes,3,opt,name=filter,proto3" json:"filter,omitempty"`
|
Filter string `protobuf:"bytes,3,opt,name=filter,proto3" json:"filter,omitempty"`
|
||||||
|
Icon *MemoViewsUserSetting_MemoView_Icon `protobuf:"bytes,4,opt,name=icon,proto3" json:"icon,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
|
|
@ -916,6 +917,97 @@ func (x *MemoViewsUserSetting_MemoView) GetFilter() string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *MemoViewsUserSetting_MemoView) GetIcon() *MemoViewsUserSetting_MemoView_Icon {
|
||||||
|
if x != nil {
|
||||||
|
return x.Icon
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type MemoViewsUserSetting_MemoView_Icon struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
// Types that are valid to be assigned to Value:
|
||||||
|
//
|
||||||
|
// *MemoViewsUserSetting_MemoView_Icon_Emoji
|
||||||
|
// *MemoViewsUserSetting_MemoView_Icon_Lucide
|
||||||
|
Value isMemoViewsUserSetting_MemoView_Icon_Value `protobuf_oneof:"value"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MemoViewsUserSetting_MemoView_Icon) Reset() {
|
||||||
|
*x = MemoViewsUserSetting_MemoView_Icon{}
|
||||||
|
mi := &file_store_user_setting_proto_msgTypes[13]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MemoViewsUserSetting_MemoView_Icon) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*MemoViewsUserSetting_MemoView_Icon) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *MemoViewsUserSetting_MemoView_Icon) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_store_user_setting_proto_msgTypes[13]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use MemoViewsUserSetting_MemoView_Icon.ProtoReflect.Descriptor instead.
|
||||||
|
func (*MemoViewsUserSetting_MemoView_Icon) Descriptor() ([]byte, []int) {
|
||||||
|
return file_store_user_setting_proto_rawDescGZIP(), []int{6, 0, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MemoViewsUserSetting_MemoView_Icon) GetValue() isMemoViewsUserSetting_MemoView_Icon_Value {
|
||||||
|
if x != nil {
|
||||||
|
return x.Value
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MemoViewsUserSetting_MemoView_Icon) GetEmoji() string {
|
||||||
|
if x != nil {
|
||||||
|
if x, ok := x.Value.(*MemoViewsUserSetting_MemoView_Icon_Emoji); ok {
|
||||||
|
return x.Emoji
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MemoViewsUserSetting_MemoView_Icon) GetLucide() string {
|
||||||
|
if x != nil {
|
||||||
|
if x, ok := x.Value.(*MemoViewsUserSetting_MemoView_Icon_Lucide); ok {
|
||||||
|
return x.Lucide
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type isMemoViewsUserSetting_MemoView_Icon_Value interface {
|
||||||
|
isMemoViewsUserSetting_MemoView_Icon_Value()
|
||||||
|
}
|
||||||
|
|
||||||
|
type MemoViewsUserSetting_MemoView_Icon_Emoji struct {
|
||||||
|
// A Unicode emoji sequence.
|
||||||
|
Emoji string `protobuf:"bytes,1,opt,name=emoji,proto3,oneof"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MemoViewsUserSetting_MemoView_Icon_Lucide struct {
|
||||||
|
// A canonical Lucide icon name, such as "leaf".
|
||||||
|
Lucide string `protobuf:"bytes,2,opt,name=lucide,proto3,oneof"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*MemoViewsUserSetting_MemoView_Icon_Emoji) isMemoViewsUserSetting_MemoView_Icon_Value() {}
|
||||||
|
|
||||||
|
func (*MemoViewsUserSetting_MemoView_Icon_Lucide) isMemoViewsUserSetting_MemoView_Icon_Value() {}
|
||||||
|
|
||||||
type WebhooksUserSetting_Webhook struct {
|
type WebhooksUserSetting_Webhook struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
// Unique identifier for the webhook
|
// Unique identifier for the webhook
|
||||||
|
|
@ -932,7 +1024,7 @@ type WebhooksUserSetting_Webhook struct {
|
||||||
|
|
||||||
func (x *WebhooksUserSetting_Webhook) Reset() {
|
func (x *WebhooksUserSetting_Webhook) Reset() {
|
||||||
*x = WebhooksUserSetting_Webhook{}
|
*x = WebhooksUserSetting_Webhook{}
|
||||||
mi := &file_store_user_setting_proto_msgTypes[13]
|
mi := &file_store_user_setting_proto_msgTypes[14]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
|
|
@ -944,7 +1036,7 @@ func (x *WebhooksUserSetting_Webhook) String() string {
|
||||||
func (*WebhooksUserSetting_Webhook) ProtoMessage() {}
|
func (*WebhooksUserSetting_Webhook) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *WebhooksUserSetting_Webhook) ProtoReflect() protoreflect.Message {
|
func (x *WebhooksUserSetting_Webhook) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_store_user_setting_proto_msgTypes[13]
|
mi := &file_store_user_setting_proto_msgTypes[14]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
|
@ -1060,14 +1152,19 @@ const file_store_user_setting_proto_rawDesc = "" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"created_at\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12<\n" +
|
"created_at\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12<\n" +
|
||||||
"\flast_used_at\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\n" +
|
"\flast_used_at\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\n" +
|
||||||
"lastUsedAt\"\xab\x01\n" +
|
"lastUsedAt\"\xb4\x02\n" +
|
||||||
"\x14MemoViewsUserSetting\x12I\n" +
|
"\x14MemoViewsUserSetting\x12I\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"memo_views\x18\x01 \x03(\v2*.memos.store.MemoViewsUserSetting.MemoViewR\tmemoViews\x1aH\n" +
|
"memo_views\x18\x01 \x03(\v2*.memos.store.MemoViewsUserSetting.MemoViewR\tmemoViews\x1a\xd0\x01\n" +
|
||||||
"\bMemoView\x12\x0e\n" +
|
"\bMemoView\x12\x0e\n" +
|
||||||
"\x02id\x18\x01 \x01(\tR\x02id\x12\x14\n" +
|
"\x02id\x18\x01 \x01(\tR\x02id\x12\x14\n" +
|
||||||
"\x05title\x18\x02 \x01(\tR\x05title\x12\x16\n" +
|
"\x05title\x18\x02 \x01(\tR\x05title\x12\x16\n" +
|
||||||
"\x06filter\x18\x03 \x01(\tR\x06filter\"\xc5\x01\n" +
|
"\x06filter\x18\x03 \x01(\tR\x06filter\x12C\n" +
|
||||||
|
"\x04icon\x18\x04 \x01(\v2/.memos.store.MemoViewsUserSetting.MemoView.IconR\x04icon\x1aA\n" +
|
||||||
|
"\x04Icon\x12\x16\n" +
|
||||||
|
"\x05emoji\x18\x01 \x01(\tH\x00R\x05emoji\x12\x18\n" +
|
||||||
|
"\x06lucide\x18\x02 \x01(\tH\x00R\x06lucideB\a\n" +
|
||||||
|
"\x05value\"\xc5\x01\n" +
|
||||||
"\x13WebhooksUserSetting\x12D\n" +
|
"\x13WebhooksUserSetting\x12D\n" +
|
||||||
"\bwebhooks\x18\x01 \x03(\v2(.memos.store.WebhooksUserSetting.WebhookR\bwebhooks\x1ah\n" +
|
"\bwebhooks\x18\x01 \x03(\v2(.memos.store.WebhooksUserSetting.WebhookR\bwebhooks\x1ah\n" +
|
||||||
"\aWebhook\x12\x0e\n" +
|
"\aWebhook\x12\x0e\n" +
|
||||||
|
|
@ -1090,7 +1187,7 @@ func file_store_user_setting_proto_rawDescGZIP() []byte {
|
||||||
}
|
}
|
||||||
|
|
||||||
var file_store_user_setting_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
|
var file_store_user_setting_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
|
||||||
var file_store_user_setting_proto_msgTypes = make([]protoimpl.MessageInfo, 14)
|
var file_store_user_setting_proto_msgTypes = make([]protoimpl.MessageInfo, 15)
|
||||||
var file_store_user_setting_proto_goTypes = []any{
|
var file_store_user_setting_proto_goTypes = []any{
|
||||||
(UserSetting_Key)(0), // 0: memos.store.UserSetting.Key
|
(UserSetting_Key)(0), // 0: memos.store.UserSetting.Key
|
||||||
(*UserSetting)(nil), // 1: memos.store.UserSetting
|
(*UserSetting)(nil), // 1: memos.store.UserSetting
|
||||||
|
|
@ -1106,9 +1203,10 @@ var file_store_user_setting_proto_goTypes = []any{
|
||||||
(*RefreshTokensUserSetting_ClientInfo)(nil), // 11: memos.store.RefreshTokensUserSetting.ClientInfo
|
(*RefreshTokensUserSetting_ClientInfo)(nil), // 11: memos.store.RefreshTokensUserSetting.ClientInfo
|
||||||
(*PersonalAccessTokensUserSetting_PersonalAccessToken)(nil), // 12: memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken
|
(*PersonalAccessTokensUserSetting_PersonalAccessToken)(nil), // 12: memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken
|
||||||
(*MemoViewsUserSetting_MemoView)(nil), // 13: memos.store.MemoViewsUserSetting.MemoView
|
(*MemoViewsUserSetting_MemoView)(nil), // 13: memos.store.MemoViewsUserSetting.MemoView
|
||||||
(*WebhooksUserSetting_Webhook)(nil), // 14: memos.store.WebhooksUserSetting.Webhook
|
(*MemoViewsUserSetting_MemoView_Icon)(nil), // 14: memos.store.MemoViewsUserSetting.MemoView.Icon
|
||||||
(*color.Color)(nil), // 15: google.type.Color
|
(*WebhooksUserSetting_Webhook)(nil), // 15: memos.store.WebhooksUserSetting.Webhook
|
||||||
(*timestamppb.Timestamp)(nil), // 16: google.protobuf.Timestamp
|
(*color.Color)(nil), // 16: google.type.Color
|
||||||
|
(*timestamppb.Timestamp)(nil), // 17: google.protobuf.Timestamp
|
||||||
}
|
}
|
||||||
var file_store_user_setting_proto_depIdxs = []int32{
|
var file_store_user_setting_proto_depIdxs = []int32{
|
||||||
0, // 0: memos.store.UserSetting.key:type_name -> memos.store.UserSetting.Key
|
0, // 0: memos.store.UserSetting.key:type_name -> memos.store.UserSetting.Key
|
||||||
|
|
@ -1118,24 +1216,25 @@ var file_store_user_setting_proto_depIdxs = []int32{
|
||||||
5, // 4: memos.store.UserSetting.refresh_tokens:type_name -> memos.store.RefreshTokensUserSetting
|
5, // 4: memos.store.UserSetting.refresh_tokens:type_name -> memos.store.RefreshTokensUserSetting
|
||||||
6, // 5: memos.store.UserSetting.personal_access_tokens:type_name -> memos.store.PersonalAccessTokensUserSetting
|
6, // 5: memos.store.UserSetting.personal_access_tokens:type_name -> memos.store.PersonalAccessTokensUserSetting
|
||||||
4, // 6: memos.store.UserSetting.tags:type_name -> memos.store.TagsUserSetting
|
4, // 6: memos.store.UserSetting.tags:type_name -> memos.store.TagsUserSetting
|
||||||
15, // 7: memos.store.UserTagMetadata.background_color:type_name -> google.type.Color
|
16, // 7: memos.store.UserTagMetadata.background_color:type_name -> google.type.Color
|
||||||
9, // 8: memos.store.TagsUserSetting.tags:type_name -> memos.store.TagsUserSetting.TagsEntry
|
9, // 8: memos.store.TagsUserSetting.tags:type_name -> memos.store.TagsUserSetting.TagsEntry
|
||||||
10, // 9: memos.store.RefreshTokensUserSetting.refresh_tokens:type_name -> memos.store.RefreshTokensUserSetting.RefreshToken
|
10, // 9: memos.store.RefreshTokensUserSetting.refresh_tokens:type_name -> memos.store.RefreshTokensUserSetting.RefreshToken
|
||||||
12, // 10: memos.store.PersonalAccessTokensUserSetting.tokens:type_name -> memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken
|
12, // 10: memos.store.PersonalAccessTokensUserSetting.tokens:type_name -> memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken
|
||||||
13, // 11: memos.store.MemoViewsUserSetting.memo_views:type_name -> memos.store.MemoViewsUserSetting.MemoView
|
13, // 11: memos.store.MemoViewsUserSetting.memo_views:type_name -> memos.store.MemoViewsUserSetting.MemoView
|
||||||
14, // 12: memos.store.WebhooksUserSetting.webhooks:type_name -> memos.store.WebhooksUserSetting.Webhook
|
15, // 12: memos.store.WebhooksUserSetting.webhooks:type_name -> memos.store.WebhooksUserSetting.Webhook
|
||||||
3, // 13: memos.store.TagsUserSetting.TagsEntry.value:type_name -> memos.store.UserTagMetadata
|
3, // 13: memos.store.TagsUserSetting.TagsEntry.value:type_name -> memos.store.UserTagMetadata
|
||||||
16, // 14: memos.store.RefreshTokensUserSetting.RefreshToken.expires_at:type_name -> google.protobuf.Timestamp
|
17, // 14: memos.store.RefreshTokensUserSetting.RefreshToken.expires_at:type_name -> google.protobuf.Timestamp
|
||||||
16, // 15: memos.store.RefreshTokensUserSetting.RefreshToken.created_at:type_name -> google.protobuf.Timestamp
|
17, // 15: memos.store.RefreshTokensUserSetting.RefreshToken.created_at:type_name -> google.protobuf.Timestamp
|
||||||
11, // 16: memos.store.RefreshTokensUserSetting.RefreshToken.client_info:type_name -> memos.store.RefreshTokensUserSetting.ClientInfo
|
11, // 16: memos.store.RefreshTokensUserSetting.RefreshToken.client_info:type_name -> memos.store.RefreshTokensUserSetting.ClientInfo
|
||||||
16, // 17: memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken.expires_at:type_name -> google.protobuf.Timestamp
|
17, // 17: memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken.expires_at:type_name -> google.protobuf.Timestamp
|
||||||
16, // 18: memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken.created_at:type_name -> google.protobuf.Timestamp
|
17, // 18: memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken.created_at:type_name -> google.protobuf.Timestamp
|
||||||
16, // 19: memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken.last_used_at:type_name -> google.protobuf.Timestamp
|
17, // 19: memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken.last_used_at:type_name -> google.protobuf.Timestamp
|
||||||
20, // [20:20] is the sub-list for method output_type
|
14, // 20: memos.store.MemoViewsUserSetting.MemoView.icon:type_name -> memos.store.MemoViewsUserSetting.MemoView.Icon
|
||||||
20, // [20:20] is the sub-list for method input_type
|
21, // [21:21] is the sub-list for method output_type
|
||||||
20, // [20:20] is the sub-list for extension type_name
|
21, // [21:21] is the sub-list for method input_type
|
||||||
20, // [20:20] is the sub-list for extension extendee
|
21, // [21:21] is the sub-list for extension type_name
|
||||||
0, // [0:20] is the sub-list for field type_name
|
21, // [21:21] is the sub-list for extension extendee
|
||||||
|
0, // [0:21] is the sub-list for field type_name
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() { file_store_user_setting_proto_init() }
|
func init() { file_store_user_setting_proto_init() }
|
||||||
|
|
@ -1151,13 +1250,17 @@ func file_store_user_setting_proto_init() {
|
||||||
(*UserSetting_PersonalAccessTokens)(nil),
|
(*UserSetting_PersonalAccessTokens)(nil),
|
||||||
(*UserSetting_Tags)(nil),
|
(*UserSetting_Tags)(nil),
|
||||||
}
|
}
|
||||||
|
file_store_user_setting_proto_msgTypes[13].OneofWrappers = []any{
|
||||||
|
(*MemoViewsUserSetting_MemoView_Icon_Emoji)(nil),
|
||||||
|
(*MemoViewsUserSetting_MemoView_Icon_Lucide)(nil),
|
||||||
|
}
|
||||||
type x struct{}
|
type x struct{}
|
||||||
out := protoimpl.TypeBuilder{
|
out := protoimpl.TypeBuilder{
|
||||||
File: protoimpl.DescBuilder{
|
File: protoimpl.DescBuilder{
|
||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_store_user_setting_proto_rawDesc), len(file_store_user_setting_proto_rawDesc)),
|
RawDescriptor: unsafe.Slice(unsafe.StringData(file_store_user_setting_proto_rawDesc), len(file_store_user_setting_proto_rawDesc)),
|
||||||
NumEnums: 1,
|
NumEnums: 1,
|
||||||
NumMessages: 14,
|
NumMessages: 15,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 0,
|
NumServices: 0,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,16 @@ message MemoViewsUserSetting {
|
||||||
string id = 1;
|
string id = 1;
|
||||||
string title = 2;
|
string title = 2;
|
||||||
string filter = 3;
|
string filter = 3;
|
||||||
|
Icon icon = 4;
|
||||||
|
|
||||||
|
message Icon {
|
||||||
|
oneof value {
|
||||||
|
// A Unicode emoji sequence.
|
||||||
|
string emoji = 1;
|
||||||
|
// A canonical Lucide icon name, such as "leaf".
|
||||||
|
string lucide = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
repeated MemoView memo_views = 1;
|
repeated MemoView memo_views = 1;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
35
server/router/api/v1/memo_view_icon.go
Normal file
35
server/router/api/v1/memo_view_icon.go
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
v1pb "github.com/usememos/memos/proto/gen/api/v1"
|
||||||
|
storepb "github.com/usememos/memos/proto/gen/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
func convertMemoViewIconFromStore(icon *storepb.MemoViewsUserSetting_MemoView_Icon) *v1pb.MemoView_Icon {
|
||||||
|
if icon == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
switch value := icon.Value.(type) {
|
||||||
|
case *storepb.MemoViewsUserSetting_MemoView_Icon_Emoji:
|
||||||
|
return &v1pb.MemoView_Icon{Value: &v1pb.MemoView_Icon_Emoji{Emoji: value.Emoji}}
|
||||||
|
case *storepb.MemoViewsUserSetting_MemoView_Icon_Lucide:
|
||||||
|
return &v1pb.MemoView_Icon{Value: &v1pb.MemoView_Icon_Lucide{Lucide: value.Lucide}}
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertMemoViewIconToStore(icon *v1pb.MemoView_Icon) *storepb.MemoViewsUserSetting_MemoView_Icon {
|
||||||
|
if icon == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
switch value := icon.Value.(type) {
|
||||||
|
case *v1pb.MemoView_Icon_Emoji:
|
||||||
|
return &storepb.MemoViewsUserSetting_MemoView_Icon{Value: &storepb.MemoViewsUserSetting_MemoView_Icon_Emoji{Emoji: value.Emoji}}
|
||||||
|
case *v1pb.MemoView_Icon_Lucide:
|
||||||
|
return &storepb.MemoViewsUserSetting_MemoView_Icon{Value: &storepb.MemoViewsUserSetting_MemoView_Icon_Lucide{Lucide: value.Lucide}}
|
||||||
|
default:
|
||||||
|
// Preserve an empty icon so validation rejects it instead of treating it as a reset.
|
||||||
|
return &storepb.MemoViewsUserSetting_MemoView_Icon{}
|
||||||
|
}
|
||||||
|
}
|
||||||
99
server/router/api/v1/test/memo_view_icon_test.go
Normal file
99
server/router/api/v1/test/memo_view_icon_test.go
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
package test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
"google.golang.org/protobuf/types/known/fieldmaskpb"
|
||||||
|
|
||||||
|
v1pb "github.com/usememos/memos/proto/gen/api/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMemoViewIconLifecycle(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
ts := NewTestService(t)
|
||||||
|
defer ts.Cleanup()
|
||||||
|
owner, err := ts.CreateRegularUser(ctx, "icon-owner")
|
||||||
|
require.NoError(t, err)
|
||||||
|
other, err := ts.CreateRegularUser(ctx, "icon-other")
|
||||||
|
require.NoError(t, err)
|
||||||
|
ownerCtx := ts.CreateUserContext(ctx, owner.ID)
|
||||||
|
parent := "users/" + owner.Username
|
||||||
|
emoji := &v1pb.MemoView_Icon{Value: &v1pb.MemoView_Icon_Emoji{Emoji: "👩🏽🌾"}}
|
||||||
|
request := &v1pb.CreateMemoViewRequest{
|
||||||
|
Parent: parent,
|
||||||
|
MemoView: &v1pb.MemoView{Title: "Garden", Filter: "pinned", Icon: emoji},
|
||||||
|
ValidateOnly: true,
|
||||||
|
}
|
||||||
|
validated, err := ts.Service.CreateMemoView(ownerCtx, request)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, proto.Equal(emoji, validated.Icon))
|
||||||
|
listed, err := ts.Service.ListMemoViews(ownerCtx, &v1pb.ListMemoViewsRequest{Parent: parent})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Empty(t, listed.MemoViews)
|
||||||
|
|
||||||
|
request.ValidateOnly = false
|
||||||
|
view, err := ts.Service.CreateMemoView(ownerCtx, request)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, proto.Equal(emoji, view.Icon))
|
||||||
|
view, err = ts.Service.UpdateMemoView(ownerCtx, &v1pb.UpdateMemoViewRequest{
|
||||||
|
MemoView: &v1pb.MemoView{Name: view.Name, Title: "Renamed", Filter: "has_link"},
|
||||||
|
UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"title", "filter"}},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, proto.Equal(emoji, view.Icon), "other field updates preserve the icon")
|
||||||
|
|
||||||
|
lucide := &v1pb.MemoView_Icon{Value: &v1pb.MemoView_Icon_Lucide{Lucide: "future-icon"}}
|
||||||
|
update := &v1pb.UpdateMemoViewRequest{
|
||||||
|
MemoView: &v1pb.MemoView{Name: view.Name, Icon: lucide},
|
||||||
|
UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"icon"}},
|
||||||
|
}
|
||||||
|
_, err = ts.Service.UpdateMemoView(ts.CreateUserContext(ctx, other.ID), update)
|
||||||
|
require.Equal(t, codes.PermissionDenied, status.Code(err))
|
||||||
|
view, err = ts.Service.UpdateMemoView(ownerCtx, update)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, proto.Equal(lucide, view.Icon))
|
||||||
|
require.Equal(t, "Renamed", view.Title)
|
||||||
|
require.Equal(t, "has_link", view.Filter)
|
||||||
|
fetched, err := ts.Service.GetMemoView(ownerCtx, &v1pb.GetMemoViewRequest{Name: view.Name})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, proto.Equal(lucide, fetched.Icon))
|
||||||
|
listed, err = ts.Service.ListMemoViews(ownerCtx, &v1pb.ListMemoViewsRequest{Parent: parent})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, listed.MemoViews, 1)
|
||||||
|
require.True(t, proto.Equal(lucide, listed.MemoViews[0].Icon))
|
||||||
|
|
||||||
|
for _, invalid := range []*v1pb.MemoView_Icon{
|
||||||
|
{},
|
||||||
|
{Value: &v1pb.MemoView_Icon_Emoji{Emoji: "🌱🌱"}},
|
||||||
|
{Value: &v1pb.MemoView_Icon_Emoji{Emoji: ""}},
|
||||||
|
{Value: &v1pb.MemoView_Icon_Lucide{Lucide: "BookOpen"}},
|
||||||
|
{Value: &v1pb.MemoView_Icon_Lucide{Lucide: ""}},
|
||||||
|
} {
|
||||||
|
request.MemoView.Icon = invalid
|
||||||
|
request.ValidateOnly = true
|
||||||
|
_, err = ts.Service.CreateMemoView(ownerCtx, request)
|
||||||
|
require.Equal(t, codes.InvalidArgument, status.Code(err))
|
||||||
|
request.ValidateOnly = false
|
||||||
|
_, err = ts.Service.CreateMemoView(ownerCtx, request)
|
||||||
|
require.Equal(t, codes.InvalidArgument, status.Code(err))
|
||||||
|
update.MemoView.Icon = invalid
|
||||||
|
_, err = ts.Service.UpdateMemoView(ownerCtx, update)
|
||||||
|
require.Equal(t, codes.InvalidArgument, status.Code(err))
|
||||||
|
}
|
||||||
|
fetched, err = ts.Service.GetMemoView(ownerCtx, &v1pb.GetMemoViewRequest{Name: view.Name})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, proto.Equal(lucide, fetched.Icon), "invalid updates leave storage unchanged")
|
||||||
|
|
||||||
|
update.MemoView.Icon = nil
|
||||||
|
view, err = ts.Service.UpdateMemoView(ownerCtx, update)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Nil(t, view.Icon)
|
||||||
|
fetched, err = ts.Service.GetMemoView(ownerCtx, &v1pb.GetMemoViewRequest{Name: view.Name})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Nil(t, fetched.Icon)
|
||||||
|
}
|
||||||
|
|
@ -51,6 +51,7 @@ func convertMemoViewFromStore(username string, memoView *storepb.MemoViewsUserSe
|
||||||
Name: constructMemoViewName(username, memoView.GetId()),
|
Name: constructMemoViewName(username, memoView.GetId()),
|
||||||
Title: memoView.GetTitle(),
|
Title: memoView.GetTitle(),
|
||||||
Filter: memoView.GetFilter(),
|
Filter: memoView.GetFilter(),
|
||||||
|
Icon: convertMemoViewIconFromStore(memoView.GetIcon()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -138,6 +139,7 @@ func (s *APIV1Service) CreateMemoView(ctx context.Context, request *v1pb.CreateM
|
||||||
Id: util.GenUUID(),
|
Id: util.GenUUID(),
|
||||||
Title: request.GetMemoView().GetTitle(),
|
Title: request.GetMemoView().GetTitle(),
|
||||||
Filter: request.GetMemoView().GetFilter(),
|
Filter: request.GetMemoView().GetFilter(),
|
||||||
|
Icon: convertMemoViewIconToStore(request.GetMemoView().GetIcon()),
|
||||||
}
|
}
|
||||||
if newMemoView.Title == "" {
|
if newMemoView.Title == "" {
|
||||||
return nil, status.Errorf(codes.InvalidArgument, "title is required")
|
return nil, status.Errorf(codes.InvalidArgument, "title is required")
|
||||||
|
|
@ -145,6 +147,9 @@ func (s *APIV1Service) CreateMemoView(ctx context.Context, request *v1pb.CreateM
|
||||||
if err := s.validateFilter(ctx, newMemoView.Filter); err != nil {
|
if err := s.validateFilter(ctx, newMemoView.Filter); err != nil {
|
||||||
return nil, status.Errorf(codes.InvalidArgument, "invalid filter: %v", err)
|
return nil, status.Errorf(codes.InvalidArgument, "invalid filter: %v", err)
|
||||||
}
|
}
|
||||||
|
if err := store.ValidateMemoViewIcon(newMemoView.Icon); err != nil {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "invalid icon: %v", err)
|
||||||
|
}
|
||||||
if request.ValidateOnly {
|
if request.ValidateOnly {
|
||||||
return convertMemoViewFromStore(user.Username, newMemoView), nil
|
return convertMemoViewFromStore(user.Username, newMemoView), nil
|
||||||
}
|
}
|
||||||
|
|
@ -170,6 +175,7 @@ func (s *APIV1Service) UpdateMemoView(ctx context.Context, request *v1pb.UpdateM
|
||||||
}
|
}
|
||||||
|
|
||||||
var title, filterValue *string
|
var title, filterValue *string
|
||||||
|
var icon **storepb.MemoViewsUserSetting_MemoView_Icon
|
||||||
for _, field := range request.UpdateMask.Paths {
|
for _, field := range request.UpdateMask.Paths {
|
||||||
switch field {
|
switch field {
|
||||||
case "title":
|
case "title":
|
||||||
|
|
@ -184,12 +190,18 @@ func (s *APIV1Service) UpdateMemoView(ctx context.Context, request *v1pb.UpdateM
|
||||||
}
|
}
|
||||||
value := request.GetMemoView().GetFilter()
|
value := request.GetMemoView().GetFilter()
|
||||||
filterValue = &value
|
filterValue = &value
|
||||||
|
case "icon":
|
||||||
|
value := convertMemoViewIconToStore(request.GetMemoView().GetIcon())
|
||||||
|
if err := store.ValidateMemoViewIcon(value); err != nil {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "invalid icon: %v", err)
|
||||||
|
}
|
||||||
|
icon = &value
|
||||||
default:
|
default:
|
||||||
return nil, status.Errorf(codes.InvalidArgument, "unsupported update mask path: %s", field)
|
return nil, status.Errorf(codes.InvalidArgument, "unsupported update mask path: %s", field)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updatedMemoView, err := s.Store.UpdateUserMemoView(ctx, user.ID, memoViewID, title, filterValue)
|
updatedMemoView, err := s.Store.UpdateUserMemoView(ctx, user.ID, memoViewID, title, filterValue, icon)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Errorf(codes.Internal, "failed to update memo view: %v", err)
|
return nil, status.Errorf(codes.Internal, "failed to update memo view: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
33
store/custom_icon.go
Normal file
33
store/custom_icon.go
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/usememos/memos/internal/markdown/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
func validateEmojiIcon(emoji string) error {
|
||||||
|
if !parser.IsEmoji(emoji) {
|
||||||
|
return errors.New("icon must contain one fully qualified emoji")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateLucideIcon(name string) error {
|
||||||
|
if len(name) == 0 || len(name) > 128 {
|
||||||
|
return errors.New("icon name must contain 1 to 128 bytes")
|
||||||
|
}
|
||||||
|
for _, part := range strings.Split(name, "-") {
|
||||||
|
if part == "" {
|
||||||
|
return errors.New("icon name must use lowercase kebab-case")
|
||||||
|
}
|
||||||
|
for _, char := range part {
|
||||||
|
if (char < 'a' || char > 'z') && (char < '0' || char > '9') {
|
||||||
|
return errors.New("icon name must use lowercase kebab-case")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
22
store/memo_view_icon.go
Normal file
22
store/memo_view_icon.go
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
storepb "github.com/usememos/memos/proto/gen/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ValidateMemoViewIcon validates optional display metadata for a saved view.
|
||||||
|
func ValidateMemoViewIcon(icon *storepb.MemoViewsUserSetting_MemoView_Icon) error {
|
||||||
|
if icon == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
switch value := icon.Value.(type) {
|
||||||
|
case *storepb.MemoViewsUserSetting_MemoView_Icon_Emoji:
|
||||||
|
return validateEmojiIcon(value.Emoji)
|
||||||
|
case *storepb.MemoViewsUserSetting_MemoView_Icon_Lucide:
|
||||||
|
return validateLucideIcon(value.Lucide)
|
||||||
|
default:
|
||||||
|
return errors.New("view icon must specify emoji or lucide")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,8 @@
|
||||||
package store
|
package store
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/usememos/memos/internal/markdown/parser"
|
|
||||||
storepb "github.com/usememos/memos/proto/gen/store"
|
storepb "github.com/usememos/memos/proto/gen/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -17,25 +14,10 @@ func ValidateSpaceIcon(icon *storepb.SpacePayload_Icon) error {
|
||||||
}
|
}
|
||||||
switch value := icon.Value.(type) {
|
switch value := icon.Value.(type) {
|
||||||
case *storepb.SpacePayload_Icon_Emoji:
|
case *storepb.SpacePayload_Icon_Emoji:
|
||||||
if !parser.IsEmoji(value.Emoji) {
|
return validateEmojiIcon(value.Emoji)
|
||||||
return errors.New("space icon must contain one fully qualified emoji")
|
|
||||||
}
|
|
||||||
case *storepb.SpacePayload_Icon_Lucide:
|
case *storepb.SpacePayload_Icon_Lucide:
|
||||||
if len(value.Lucide) == 0 || len(value.Lucide) > 128 {
|
return validateLucideIcon(value.Lucide)
|
||||||
return errors.New("space icon name must contain 1 to 128 bytes")
|
|
||||||
}
|
|
||||||
for _, part := range strings.Split(value.Lucide, "-") {
|
|
||||||
if part == "" {
|
|
||||||
return errors.New("space icon name must use lowercase kebab-case")
|
|
||||||
}
|
|
||||||
for _, char := range part {
|
|
||||||
if (char < 'a' || char > 'z') && (char < '0' || char > '9') {
|
|
||||||
return errors.New("space icon name must use lowercase kebab-case")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
return errors.New("space icon must specify emoji or lucide")
|
return errors.New("space icon must specify emoji or lucide")
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -939,21 +939,27 @@ func TestUserMemoViewConcurrentPartialUpdates(t *testing.T) {
|
||||||
Id: "work",
|
Id: "work",
|
||||||
Title: "Original title",
|
Title: "Original title",
|
||||||
Filter: `tag in ["original"]`,
|
Filter: `tag in ["original"]`,
|
||||||
|
Icon: &storepb.MemoViewsUserSetting_MemoView_Icon{Value: &storepb.MemoViewsUserSetting_MemoView_Icon_Emoji{Emoji: "🌱"}},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
updatedTitle := "Updated title"
|
updatedTitle := "Updated title"
|
||||||
updatedFilter := `tag in ["updated"]`
|
updatedFilter := `tag in ["updated"]`
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
errs := make([]error, 2)
|
errs := make([]error, 3)
|
||||||
start := make(chan struct{})
|
start := make(chan struct{})
|
||||||
|
|
||||||
wg.Go(func() {
|
wg.Go(func() {
|
||||||
<-start
|
<-start
|
||||||
_, errs[0] = ts.UpdateUserMemoView(ctx, user.ID, "work", &updatedTitle, nil)
|
_, errs[0] = ts.UpdateUserMemoView(ctx, user.ID, "work", &updatedTitle, nil, nil)
|
||||||
})
|
})
|
||||||
wg.Go(func() {
|
wg.Go(func() {
|
||||||
<-start
|
<-start
|
||||||
_, errs[1] = ts.UpdateUserMemoView(ctx, user.ID, "work", nil, &updatedFilter)
|
_, errs[1] = ts.UpdateUserMemoView(ctx, user.ID, "work", nil, &updatedFilter, nil)
|
||||||
|
})
|
||||||
|
wg.Go(func() {
|
||||||
|
<-start
|
||||||
|
icon := &storepb.MemoViewsUserSetting_MemoView_Icon{Value: &storepb.MemoViewsUserSetting_MemoView_Icon_Lucide{Lucide: "leaf"}}
|
||||||
|
_, errs[2] = ts.UpdateUserMemoView(ctx, user.ID, "work", nil, nil, &icon)
|
||||||
})
|
})
|
||||||
close(start)
|
close(start)
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
@ -961,11 +967,14 @@ func TestUserMemoViewConcurrentPartialUpdates(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, err = ts.ListUserSettings(ctx, &store.FindUserSetting{UserID: &user.ID, Key: storepb.UserSetting_MEMO_VIEWS})
|
||||||
|
require.NoError(t, err)
|
||||||
memoViews, err := ts.GetUserMemoViews(ctx, user.ID)
|
memoViews, err := ts.GetUserMemoViews(ctx, user.ID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, memoViews, 1)
|
require.Len(t, memoViews, 1)
|
||||||
require.Equal(t, updatedTitle, memoViews[0].GetTitle())
|
require.Equal(t, updatedTitle, memoViews[0].GetTitle())
|
||||||
require.Equal(t, updatedFilter, memoViews[0].GetFilter())
|
require.Equal(t, updatedFilter, memoViews[0].GetFilter())
|
||||||
|
require.Equal(t, "leaf", memoViews[0].GetIcon().GetLucide())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetUserMemoViewsReturnsCopies(t *testing.T) {
|
func TestGetUserMemoViewsReturnsCopies(t *testing.T) {
|
||||||
|
|
@ -980,17 +989,20 @@ func TestGetUserMemoViewsReturnsCopies(t *testing.T) {
|
||||||
Id: "work",
|
Id: "work",
|
||||||
Title: "Original title",
|
Title: "Original title",
|
||||||
Filter: `tag in ["original"]`,
|
Filter: `tag in ["original"]`,
|
||||||
|
Icon: &storepb.MemoViewsUserSetting_MemoView_Icon{Value: &storepb.MemoViewsUserSetting_MemoView_Icon_Emoji{Emoji: "🌱"}},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
firstRead, err := ts.GetUserMemoViews(ctx, user.ID)
|
firstRead, err := ts.GetUserMemoViews(ctx, user.ID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, firstRead, 1)
|
require.Len(t, firstRead, 1)
|
||||||
firstRead[0].Title = "Mutated title"
|
firstRead[0].Title = "Mutated title"
|
||||||
|
firstRead[0].Icon.Value = &storepb.MemoViewsUserSetting_MemoView_Icon_Lucide{Lucide: "leaf"}
|
||||||
|
|
||||||
secondRead, err := ts.GetUserMemoViews(ctx, user.ID)
|
secondRead, err := ts.GetUserMemoViews(ctx, user.ID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, secondRead, 1)
|
require.Len(t, secondRead, 1)
|
||||||
require.Equal(t, "Original title", secondRead[0].GetTitle())
|
require.Equal(t, "Original title", secondRead[0].GetTitle())
|
||||||
|
require.Equal(t, "🌱", secondRead[0].GetIcon().GetEmoji())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUserSettingMemoViewsPartialUpdate(t *testing.T) {
|
func TestUserSettingMemoViewsPartialUpdate(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -495,6 +495,7 @@ func (s *Store) AddUserMemoView(ctx context.Context, userID int32, memoView *sto
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateUserMemoView applies the non-nil field updates to the memo view carrying the same ID.
|
// UpdateUserMemoView applies the non-nil field updates to the memo view carrying the same ID.
|
||||||
|
// A non-nil icon update containing nil resets the icon.
|
||||||
// It returns nil when no matching memo view is found.
|
// It returns nil when no matching memo view is found.
|
||||||
func (s *Store) UpdateUserMemoView(
|
func (s *Store) UpdateUserMemoView(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
|
|
@ -502,6 +503,7 @@ func (s *Store) UpdateUserMemoView(
|
||||||
memoViewID string,
|
memoViewID string,
|
||||||
title *string,
|
title *string,
|
||||||
filter *string,
|
filter *string,
|
||||||
|
icon **storepb.MemoViewsUserSetting_MemoView_Icon,
|
||||||
) (*storepb.MemoViewsUserSetting_MemoView, error) {
|
) (*storepb.MemoViewsUserSetting_MemoView, error) {
|
||||||
s.memoViewMu.Lock()
|
s.memoViewMu.Lock()
|
||||||
defer s.memoViewMu.Unlock()
|
defer s.memoViewMu.Unlock()
|
||||||
|
|
@ -519,17 +521,16 @@ func (s *Store) UpdateUserMemoView(
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
updatedMemoView = &storepb.MemoViewsUserSetting_MemoView{
|
updatedMemoView = proto.CloneOf(item)
|
||||||
Id: item.GetId(),
|
|
||||||
Title: item.GetTitle(),
|
|
||||||
Filter: item.GetFilter(),
|
|
||||||
}
|
|
||||||
if title != nil {
|
if title != nil {
|
||||||
updatedMemoView.Title = *title
|
updatedMemoView.Title = *title
|
||||||
}
|
}
|
||||||
if filter != nil {
|
if filter != nil {
|
||||||
updatedMemoView.Filter = *filter
|
updatedMemoView.Filter = *filter
|
||||||
}
|
}
|
||||||
|
if icon != nil {
|
||||||
|
updatedMemoView.Icon = proto.CloneOf(*icon)
|
||||||
|
}
|
||||||
memoViews = append(memoViews, updatedMemoView)
|
memoViews = append(memoViews, updatedMemoView)
|
||||||
}
|
}
|
||||||
if updatedMemoView == nil {
|
if updatedMemoView == nil {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { MoreHorizontalIcon, ParenthesesIcon, PlusIcon, SquareCheckIcon } from "lucide-react";
|
import { MoreHorizontalIcon, PlusIcon, SquareCheckIcon } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import ConfirmDialog from "@/components/ConfirmDialog";
|
import ConfirmDialog from "@/components/ConfirmDialog";
|
||||||
import MemoDisplaySettingMenu from "@/components/MemoDisplaySettingMenu";
|
import MemoDisplaySettingMenu from "@/components/MemoDisplaySettingMenu";
|
||||||
|
import MemoViewIcon from "@/components/MemoViewIcon";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||||
import { userServiceClient } from "@/connect";
|
import { userServiceClient } from "@/connect";
|
||||||
|
|
@ -20,9 +21,10 @@ import type { MemoView } from "@/types/proto/api/v1/user_service_pb";
|
||||||
import { useTranslate } from "@/utils/i18n";
|
import { useTranslate } from "@/utils/i18n";
|
||||||
import SidebarRow, {
|
import SidebarRow, {
|
||||||
SIDEBAR_ROW_BOX_CLASSES,
|
SIDEBAR_ROW_BOX_CLASSES,
|
||||||
|
SIDEBAR_ROW_ICON_CLASSES,
|
||||||
SIDEBAR_ROW_LABEL_CLASSES,
|
SIDEBAR_ROW_LABEL_CLASSES,
|
||||||
SIDEBAR_ROW_SLOT_BUTTON_CLASSES,
|
SIDEBAR_ROW_SLOT_BUTTON_CLASSES,
|
||||||
SidebarRowIconSlot,
|
SIDEBAR_ROW_SLOT_CLASSES,
|
||||||
sidebarRowStateAttributes,
|
sidebarRowStateAttributes,
|
||||||
sidebarRowStateClasses,
|
sidebarRowStateClasses,
|
||||||
} from "./SidebarRow";
|
} from "./SidebarRow";
|
||||||
|
|
@ -102,7 +104,9 @@ const ViewsSection = ({ manageActive = false }: { manageActive?: boolean }) => {
|
||||||
className={cn(SIDEBAR_ROW_BOX_CLASSES, "group/view", sidebarRowStateClasses(state))}
|
className={cn(SIDEBAR_ROW_BOX_CLASSES, "group/view", sidebarRowStateClasses(state))}
|
||||||
>
|
>
|
||||||
<button type="button" onClick={() => handleView(id)} aria-pressed={active || undefined} className={SIDEBAR_ROW_LABEL_CLASSES}>
|
<button type="button" onClick={() => handleView(id)} aria-pressed={active || undefined} className={SIDEBAR_ROW_LABEL_CLASSES}>
|
||||||
<SidebarRowIconSlot icon={ParenthesesIcon} />
|
<span className={SIDEBAR_ROW_SLOT_CLASSES} aria-hidden="true">
|
||||||
|
<MemoViewIcon icon={memoView.icon} className={cn(SIDEBAR_ROW_ICON_CLASSES, "text-base")} />
|
||||||
|
</span>
|
||||||
<span className="min-w-0 flex-1 truncate">{memoView.title}</span>
|
<span className="min-w-0 flex-1 truncate">{memoView.title}</span>
|
||||||
</button>
|
</button>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
|
|
|
||||||
24
web/src/components/CustomIcon.tsx
Normal file
24
web/src/components/CustomIcon.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { AstroidIcon, type LucideIcon } from "lucide-react";
|
||||||
|
import type { CustomIconValue } from "@/lib/custom-icons";
|
||||||
|
import { getCustomSymbol } from "@/lib/custom-icons";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
icon?: CustomIconValue;
|
||||||
|
className?: string;
|
||||||
|
fallback?: LucideIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CustomIcon({ icon, className, fallback = AstroidIcon }: Props) {
|
||||||
|
if (icon?.value.case === "emoji" && icon.value.value) {
|
||||||
|
return (
|
||||||
|
<span aria-hidden className={cn("inline-flex shrink-0 items-center justify-center leading-none", className)}>
|
||||||
|
{icon.value.value}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const IconComponent = getCustomSymbol(icon?.value.case === "lucide" ? icon.value.value : "", fallback);
|
||||||
|
return <IconComponent aria-hidden className={cn("shrink-0", className)} strokeWidth={1.8} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CustomIcon;
|
||||||
192
web/src/components/CustomIconPicker.tsx
Normal file
192
web/src/components/CustomIconPicker.tsx
Normal file
|
|
@ -0,0 +1,192 @@
|
||||||
|
import { AstroidIcon, type LucideIcon, RotateCcwIcon, SearchIcon } from "lucide-react";
|
||||||
|
import { type KeyboardEvent, useId, useRef, useState } from "react";
|
||||||
|
import CustomIcon from "@/components/CustomIcon";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
|
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import type { CustomIconValue } from "@/lib/custom-icons";
|
||||||
|
import { CUSTOM_SYMBOLS, customIconsEqual } from "@/lib/custom-icons";
|
||||||
|
import { SPACE_EMOJI } from "@/lib/space-emoji";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useTranslate } from "@/utils/i18n";
|
||||||
|
import { FULLY_QUALIFIED_EMOJI } from "@/utils/tag-unicode-data";
|
||||||
|
|
||||||
|
const emojiSet = new Set(FULLY_QUALIFIED_EMOJI);
|
||||||
|
const symbolLabels: Record<string, string> = {
|
||||||
|
astroid: "Space",
|
||||||
|
"book-open": "Book",
|
||||||
|
"chart-no-axes-combined": "Chart",
|
||||||
|
"check-check": "Checklist",
|
||||||
|
"flask-conical": "Science",
|
||||||
|
"flower-2": "Flower",
|
||||||
|
"graduation-cap": "Education",
|
||||||
|
"map-pin": "Location",
|
||||||
|
"notebook-pen": "Notebook",
|
||||||
|
};
|
||||||
|
const symbols = Object.keys(CUSTOM_SYMBOLS).map((name) => ({
|
||||||
|
label: symbolLabels[name] ?? name.replaceAll("-", " ").replace(/^./, (char) => char.toUpperCase()),
|
||||||
|
icon: { value: { case: "lucide", value: name } } satisfies CustomIconValue,
|
||||||
|
}));
|
||||||
|
const emojis = SPACE_EMOJI.map(([emoji, label]) => ({ label, icon: { value: { case: "emoji", value: emoji } } satisfies CustomIconValue }));
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
value?: CustomIconValue;
|
||||||
|
onChange: (icon: CustomIconValue | undefined) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
label: string;
|
||||||
|
fallback?: LucideIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CustomIconPicker({ value, onChange, disabled, label, fallback = AstroidIcon }: Props) {
|
||||||
|
const t = useTranslate();
|
||||||
|
const id = useId();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [tab, setTab] = useState("icons");
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
const [focusedIndex, setFocusedIndex] = useState(0);
|
||||||
|
const searchRef = useRef<HTMLInputElement>(null);
|
||||||
|
const gridRef = useRef<HTMLDivElement>(null);
|
||||||
|
const search = query.trim().toLowerCase();
|
||||||
|
const options: { label: string; icon: CustomIconValue }[] = (tab === "icons" ? symbols : emojis).filter(
|
||||||
|
(option) => option.label.toLowerCase().includes(search) || option.icon.value.value?.includes(search),
|
||||||
|
);
|
||||||
|
const customEmoji = query.trim() || (value?.value.case === "emoji" ? value.value.value : "");
|
||||||
|
if (tab === "emoji" && emojiSet.has(customEmoji) && !options.some((option) => option.icon.value.value === customEmoji)) {
|
||||||
|
options.unshift({
|
||||||
|
label: t("space.icon.use-emoji", { emoji: customEmoji }),
|
||||||
|
icon: { value: { case: "emoji", value: customEmoji } },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const select = (icon?: CustomIconValue) => {
|
||||||
|
onChange(icon);
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
const changeTab = (next: string) => {
|
||||||
|
setTab(next);
|
||||||
|
setQuery("");
|
||||||
|
setFocusedIndex(0);
|
||||||
|
};
|
||||||
|
const moveFocus = (event: KeyboardEvent<HTMLButtonElement>, index: number) => {
|
||||||
|
const offsets: Record<string, number> = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -8, ArrowDown: 8 };
|
||||||
|
let next = index;
|
||||||
|
if (event.key in offsets) next = Math.max(0, Math.min(options.length - 1, index + offsets[event.key]));
|
||||||
|
else if (event.key === "Home") next = 0;
|
||||||
|
else if (event.key === "End") next = options.length - 1;
|
||||||
|
else return;
|
||||||
|
event.preventDefault();
|
||||||
|
setFocusedIndex(next);
|
||||||
|
gridRef.current?.querySelectorAll<HTMLButtonElement>("button")[next]?.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
open={open}
|
||||||
|
onOpenChange={(next) => {
|
||||||
|
if (next) {
|
||||||
|
changeTab(value?.value.case === "emoji" ? "emoji" : "icons");
|
||||||
|
}
|
||||||
|
setOpen(next);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PopoverTrigger
|
||||||
|
render={
|
||||||
|
<Button type="button" variant="outline" size="icon" className="size-8 p-0" disabled={disabled} aria-label={label} title={label} />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CustomIcon icon={value} fallback={fallback} className="size-5 text-xl" />
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent align="start" className="w-[290px] max-w-[calc(100vw-2rem)] p-2" aria-label={label} initialFocus={searchRef}>
|
||||||
|
<Tabs value={tab} onValueChange={changeTab}>
|
||||||
|
<TabsList className="mb-2 rounded-md bg-muted/60 p-0.5" aria-label={t("space.icon.kind")}>
|
||||||
|
{(["icons", "emoji"] as const).map((name) => (
|
||||||
|
<TabsTrigger
|
||||||
|
key={name}
|
||||||
|
id={`${id}-${name}`}
|
||||||
|
value={name}
|
||||||
|
aria-controls={`${id}-panel`}
|
||||||
|
tabIndex={tab === name ? 0 : -1}
|
||||||
|
className="flex-1 py-1 text-xs"
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
|
||||||
|
event.preventDefault();
|
||||||
|
const next = name === "icons" ? "emoji" : "icons";
|
||||||
|
changeTab(next);
|
||||||
|
document.getElementById(`${id}-${next}`)?.focus();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t(`space.icon.${name}`)}
|
||||||
|
</TabsTrigger>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
</Tabs>
|
||||||
|
<div id={`${id}-panel`} role="tabpanel" aria-labelledby={`${id}-${tab}`}>
|
||||||
|
<div className="relative mb-2">
|
||||||
|
<SearchIcon aria-hidden className="pointer-events-none absolute start-2 top-2 size-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
ref={searchRef}
|
||||||
|
value={query}
|
||||||
|
className="ps-8 text-sm"
|
||||||
|
aria-label={t(tab === "icons" ? "space.icon.search-icons" : "space.icon.search-emoji")}
|
||||||
|
placeholder={t(tab === "icons" ? "space.icon.search-icons" : "space.icon.search-emoji")}
|
||||||
|
onChange={(event) => {
|
||||||
|
setQuery(event.target.value);
|
||||||
|
setFocusedIndex(0);
|
||||||
|
}}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.nativeEvent.isComposing || event.keyCode === 229) return;
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
event.preventDefault();
|
||||||
|
if (options[0]) select(options[0].icon);
|
||||||
|
} else if (event.key === "ArrowDown" && options.length) {
|
||||||
|
event.preventDefault();
|
||||||
|
setFocusedIndex(0);
|
||||||
|
gridRef.current?.querySelector<HTMLButtonElement>("button")?.focus();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="max-h-56 overflow-y-auto overscroll-contain" ref={gridRef}>
|
||||||
|
{options.length ? (
|
||||||
|
<div className="grid grid-cols-8 gap-0.5">
|
||||||
|
{options.map((option, index) => (
|
||||||
|
<Button
|
||||||
|
key={`${option.icon.value.case}:${option.icon.value.value}`}
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
aria-label={option.label}
|
||||||
|
title={option.label}
|
||||||
|
aria-pressed={customIconsEqual(value, option.icon)}
|
||||||
|
tabIndex={index === Math.min(focusedIndex, options.length - 1) ? 0 : -1}
|
||||||
|
onFocus={() => setFocusedIndex(index)}
|
||||||
|
onKeyDown={(event) => moveFocus(event, index)}
|
||||||
|
onClick={() => select(option.icon)}
|
||||||
|
className={cn("rounded-md", customIconsEqual(value, option.icon) && "bg-accent ring-1 ring-inset ring-primary/40")}
|
||||||
|
>
|
||||||
|
<CustomIcon icon={option.icon} className={tab === "emoji" ? "size-5 text-xl" : "size-4"} />
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p role="status" className="py-8 text-center text-xs text-muted-foreground">
|
||||||
|
{t("space.icon.no-results")}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{tab === "emoji" && <p className="px-1 pt-2 text-xs leading-4 text-muted-foreground">{t("space.icon.emoji-help")}</p>}
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 border-t pt-1.5">
|
||||||
|
<Button type="button" variant="quiet" size="sm" className="w-full justify-start" disabled={!value} onClick={() => select()}>
|
||||||
|
<RotateCcwIcon aria-hidden className="size-3.5" />
|
||||||
|
{t("space.icon.reset")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CustomIconPicker;
|
||||||
|
|
@ -10,12 +10,13 @@ import {
|
||||||
LinkIcon,
|
LinkIcon,
|
||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
MapPinIcon,
|
MapPinIcon,
|
||||||
ParenthesesIcon,
|
|
||||||
SearchIcon,
|
SearchIcon,
|
||||||
SquareCheckIcon,
|
SquareCheckIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
|
import MemoViewIcon from "@/components/MemoViewIcon";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useAppSidebar } from "@/contexts/AppSidebarContext";
|
import { useAppSidebar } from "@/contexts/AppSidebarContext";
|
||||||
import { type FilterFactor, getMemoFilterKey, type MemoFilter, useMemoFilterContext } from "@/contexts/MemoFilterContext";
|
import { type FilterFactor, getMemoFilterKey, type MemoFilter, useMemoFilterContext } from "@/contexts/MemoFilterContext";
|
||||||
|
|
@ -83,6 +84,7 @@ const FILTER_CONFIGS: Record<FilterFactor, FilterConfig> = {
|
||||||
|
|
||||||
interface FilterChipProps {
|
interface FilterChipProps {
|
||||||
icon?: LucideIcon;
|
icon?: LucideIcon;
|
||||||
|
customIcon?: ReactNode;
|
||||||
label: string;
|
label: string;
|
||||||
onRemove: () => void;
|
onRemove: () => void;
|
||||||
/** When set, the label is a button that edits the filter; the label is also shown in full as a tooltip. */
|
/** When set, the label is a button that edits the filter; the label is also shown in full as a tooltip. */
|
||||||
|
|
@ -90,10 +92,10 @@ interface FilterChipProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** One chip for anything narrowing the list, so a view, a tag and a day are announced the same way. */
|
/** One chip for anything narrowing the list, so a view, a tag and a day are announced the same way. */
|
||||||
const FilterChip = ({ icon: Icon, label, onRemove, onEdit }: FilterChipProps) => {
|
const FilterChip = ({ icon: Icon, customIcon, label, onRemove, onEdit }: FilterChipProps) => {
|
||||||
const body = (
|
const body = (
|
||||||
<>
|
<>
|
||||||
{Icon && <Icon className="w-3.5 h-3.5 text-muted-foreground shrink-0" />}
|
{customIcon ?? (Icon && <Icon className="w-3.5 h-3.5 text-muted-foreground shrink-0" />)}
|
||||||
<span className={cn("text-foreground/80 truncate", onEdit ? "font-mono text-xs max-w-64" : "font-medium max-w-32")}>{label}</span>
|
<span className={cn("text-foreground/80 truncate", onEdit ? "font-mono text-xs max-w-64" : "font-medium max-w-32")}>{label}</span>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
@ -151,8 +153,13 @@ const MemoFilters = ({ className }: { className?: string }) => {
|
||||||
const viewChip = (() => {
|
const viewChip = (() => {
|
||||||
if (!viewApplies) return null;
|
if (!viewApplies) return null;
|
||||||
if (memoView === BUILTIN_TASKS_VIEW_ID) return { icon: SquareCheckIcon, label: t("common.tasks") };
|
if (memoView === BUILTIN_TASKS_VIEW_ID) return { icon: SquareCheckIcon, label: t("common.tasks") };
|
||||||
const title = memoViews.find((item) => getMemoViewId(item.name) === memoView)?.title;
|
const view = memoViews.find((item) => getMemoViewId(item.name) === memoView);
|
||||||
return title ? { icon: ParenthesesIcon, label: title } : null;
|
return view?.title
|
||||||
|
? {
|
||||||
|
customIcon: <MemoViewIcon icon={view.icon} className="size-3.5 text-sm text-muted-foreground" />,
|
||||||
|
label: view.title,
|
||||||
|
}
|
||||||
|
: null;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
if (filters.length === 0 && !viewChip) {
|
if (filters.length === 0 && !viewChip) {
|
||||||
|
|
@ -161,7 +168,9 @@ const MemoFilters = ({ className }: { className?: string }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("w-full flex flex-row justify-start items-center flex-wrap gap-2", className)}>
|
<div className={cn("w-full flex flex-row justify-start items-center flex-wrap gap-2", className)}>
|
||||||
{viewChip && <FilterChip icon={viewChip.icon} label={viewChip.label} onRemove={() => setMemoView(undefined)} />}
|
{viewChip && (
|
||||||
|
<FilterChip icon={viewChip.icon} customIcon={viewChip.customIcon} label={viewChip.label} onRemove={() => setMemoView(undefined)} />
|
||||||
|
)}
|
||||||
{filters.map((filter) => (
|
{filters.map((filter) => (
|
||||||
<FilterChip
|
<FilterChip
|
||||||
key={getMemoFilterKey(filter)}
|
key={getMemoFilterKey(filter)}
|
||||||
|
|
|
||||||
9
web/src/components/MemoViewIcon.tsx
Normal file
9
web/src/components/MemoViewIcon.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { ParenthesesIcon } from "lucide-react";
|
||||||
|
import CustomIcon from "@/components/CustomIcon";
|
||||||
|
import type { MemoView_Icon } from "@/types/proto/api/v1/user_service_pb";
|
||||||
|
|
||||||
|
const MemoViewIcon = ({ icon, className }: { icon?: MemoView_Icon; className?: string }) => (
|
||||||
|
<CustomIcon icon={icon} className={className} fallback={ParenthesesIcon} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export default MemoViewIcon;
|
||||||
|
|
@ -42,10 +42,10 @@ import {
|
||||||
useUserSpaceInvitations,
|
useUserSpaceInvitations,
|
||||||
} from "@/hooks/useSpaceQueries";
|
} from "@/hooks/useSpaceQueries";
|
||||||
import { useUsersByUsernames } from "@/hooks/useUserQueries";
|
import { useUsersByUsernames } from "@/hooks/useUserQueries";
|
||||||
|
import { customIconsEqual } from "@/lib/custom-icons";
|
||||||
import { handleError } from "@/lib/error";
|
import { handleError } from "@/lib/error";
|
||||||
import { extractUsernameFromName } from "@/lib/resource-names";
|
import { extractUsernameFromName } from "@/lib/resource-names";
|
||||||
import { extractSpaceUidFromName } from "@/lib/space-display";
|
import { extractSpaceUidFromName } from "@/lib/space-display";
|
||||||
import { spaceIconsEqual } from "@/lib/space-icons";
|
|
||||||
import { ROUTES } from "@/router/routes";
|
import { ROUTES } from "@/router/routes";
|
||||||
import { type Space, type SpaceInvitation, type SpaceMember, SpaceMember_Role } from "@/types/proto/api/v1/space_service_pb";
|
import { type Space, type SpaceInvitation, type SpaceMember, SpaceMember_Role } from "@/types/proto/api/v1/space_service_pb";
|
||||||
import type { User } from "@/types/proto/api/v1/user_service_pb";
|
import type { User } from "@/types/proto/api/v1/user_service_pb";
|
||||||
|
|
@ -370,7 +370,7 @@ const SpaceDetail = ({ space, viewerName, onBack }: SpaceDetailProps) => {
|
||||||
{ value: String(SpaceMember_Role.USER), label: t("setting.spaces.space-user") },
|
{ value: String(SpaceMember_Role.USER), label: t("setting.spaces.space-user") },
|
||||||
{ value: String(SpaceMember_Role.ADMIN), label: t("setting.spaces.space-admin") },
|
{ value: String(SpaceMember_Role.ADMIN), label: t("setting.spaces.space-admin") },
|
||||||
];
|
];
|
||||||
const iconChanged = !spaceIconsEqual(icon, space.icon);
|
const iconChanged = !customIconsEqual(icon, space.icon);
|
||||||
const detailsChanged = title.trim() !== space.title || description.trim() !== space.description || iconChanged;
|
const detailsChanged = title.trim() !== space.title || description.trim() !== space.description || iconChanged;
|
||||||
const spaceUid = extractSpaceUidFromName(space.name);
|
const spaceUid = extractSpaceUidFromName(space.name);
|
||||||
const disambiguatedSpaceTitle = `${space.title} (${spaceUid})`;
|
const disambiguatedSpaceTitle = `${space.title} (${spaceUid})`;
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,6 @@
|
||||||
import { getSpaceSymbol } from "@/lib/space-icons";
|
import CustomIcon from "@/components/CustomIcon";
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import type { Space_Icon } from "@/types/proto/api/v1/space_service_pb";
|
import type { Space_Icon } from "@/types/proto/api/v1/space_service_pb";
|
||||||
|
|
||||||
interface Props {
|
const SpaceIcon = ({ icon, className }: { icon?: Space_Icon; className?: string }) => <CustomIcon icon={icon} className={className} />;
|
||||||
icon?: Space_Icon;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SpaceIcon({ icon, className }: Props) {
|
|
||||||
if (icon?.value.case === "emoji" && icon.value.value) {
|
|
||||||
return (
|
|
||||||
<span aria-hidden className={cn("inline-flex shrink-0 items-center justify-center leading-none", className)}>
|
|
||||||
{icon.value.value}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const IconComponent = getSpaceSymbol(icon?.value.case === "lucide" ? icon.value.value : "astroid");
|
|
||||||
return <IconComponent aria-hidden className={cn("shrink-0", className)} strokeWidth={1.8} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SpaceIcon;
|
export default SpaceIcon;
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,7 @@
|
||||||
import { create } from "@bufbuild/protobuf";
|
import { create } from "@bufbuild/protobuf";
|
||||||
import { RotateCcwIcon, SearchIcon } from "lucide-react";
|
import CustomIconPicker from "@/components/CustomIconPicker";
|
||||||
import { type KeyboardEvent, useId, useRef, useState } from "react";
|
|
||||||
import SpaceIcon from "@/components/SpaceIcon";
|
|
||||||
import SpaceMark from "@/components/SpaceMark";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
||||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
||||||
import { SPACE_EMOJI } from "@/lib/space-emoji";
|
|
||||||
import { SPACE_SYMBOLS, spaceIconsEqual } from "@/lib/space-icons";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { type Space_Icon, Space_IconSchema } from "@/types/proto/api/v1/space_service_pb";
|
import { type Space_Icon, Space_IconSchema } from "@/types/proto/api/v1/space_service_pb";
|
||||||
import { useTranslate } from "@/utils/i18n";
|
import { useTranslate } from "@/utils/i18n";
|
||||||
import { FULLY_QUALIFIED_EMOJI } from "@/utils/tag-unicode-data";
|
|
||||||
|
|
||||||
const emojiSet = new Set(FULLY_QUALIFIED_EMOJI);
|
|
||||||
const symbolLabels: Record<string, string> = {
|
|
||||||
astroid: "Space",
|
|
||||||
"book-open": "Book",
|
|
||||||
"chart-no-axes-combined": "Chart",
|
|
||||||
"check-check": "Checklist",
|
|
||||||
"flask-conical": "Science",
|
|
||||||
"flower-2": "Flower",
|
|
||||||
"graduation-cap": "Education",
|
|
||||||
"map-pin": "Location",
|
|
||||||
"notebook-pen": "Notebook",
|
|
||||||
};
|
|
||||||
const symbols = Object.keys(SPACE_SYMBOLS).map((name) => ({
|
|
||||||
label: symbolLabels[name] ?? name.replaceAll("-", " ").replace(/^./, (char) => char.toUpperCase()),
|
|
||||||
icon: create(Space_IconSchema, { value: { case: "lucide", value: name } }),
|
|
||||||
}));
|
|
||||||
const emojis = SPACE_EMOJI.map(([emoji, label]) => ({ label, icon: create(Space_IconSchema, { value: { case: "emoji", value: emoji } }) }));
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
value?: Space_Icon;
|
value?: Space_Icon;
|
||||||
|
|
@ -38,168 +9,16 @@ interface Props {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SpaceIconPicker({ value, onChange, disabled }: Props) {
|
const SpaceIconPicker = ({ value, onChange, disabled }: Props) => {
|
||||||
const t = useTranslate();
|
const t = useTranslate();
|
||||||
const id = useId();
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const [tab, setTab] = useState("icons");
|
|
||||||
const [query, setQuery] = useState("");
|
|
||||||
const [focusedIndex, setFocusedIndex] = useState(0);
|
|
||||||
const searchRef = useRef<HTMLInputElement>(null);
|
|
||||||
const gridRef = useRef<HTMLDivElement>(null);
|
|
||||||
const search = query.trim().toLowerCase();
|
|
||||||
const options = (tab === "icons" ? symbols : emojis).filter(
|
|
||||||
(option) => option.label.toLowerCase().includes(search) || option.icon.value.value?.includes(search),
|
|
||||||
);
|
|
||||||
const customEmoji = query.trim() || (value?.value.case === "emoji" ? value.value.value : "");
|
|
||||||
if (tab === "emoji" && emojiSet.has(customEmoji) && !options.some((option) => option.icon.value.value === customEmoji)) {
|
|
||||||
options.unshift({
|
|
||||||
label: t("space.icon.use-emoji", { emoji: customEmoji }),
|
|
||||||
icon: create(Space_IconSchema, { value: { case: "emoji", value: customEmoji } }),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const select = (icon?: Space_Icon) => {
|
|
||||||
onChange(icon);
|
|
||||||
setOpen(false);
|
|
||||||
};
|
|
||||||
const changeTab = (next: string) => {
|
|
||||||
setTab(next);
|
|
||||||
setQuery("");
|
|
||||||
setFocusedIndex(0);
|
|
||||||
};
|
|
||||||
const moveFocus = (event: KeyboardEvent<HTMLButtonElement>, index: number) => {
|
|
||||||
const offsets: Record<string, number> = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -8, ArrowDown: 8 };
|
|
||||||
let next = index;
|
|
||||||
if (event.key in offsets) next = Math.max(0, Math.min(options.length - 1, index + offsets[event.key]));
|
|
||||||
else if (event.key === "Home") next = 0;
|
|
||||||
else if (event.key === "End") next = options.length - 1;
|
|
||||||
else return;
|
|
||||||
event.preventDefault();
|
|
||||||
setFocusedIndex(next);
|
|
||||||
gridRef.current?.querySelectorAll<HTMLButtonElement>("button")[next]?.focus();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<CustomIconPicker
|
||||||
open={open}
|
value={value}
|
||||||
onOpenChange={(next) => {
|
onChange={(icon) => onChange(icon ? create(Space_IconSchema, icon) : undefined)}
|
||||||
if (next) {
|
disabled={disabled}
|
||||||
changeTab(value?.value.case === "emoji" ? "emoji" : "icons");
|
label={t("space.icon.change")}
|
||||||
}
|
/>
|
||||||
setOpen(next);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<PopoverTrigger
|
|
||||||
render={
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
className="size-8 p-0"
|
|
||||||
disabled={disabled}
|
|
||||||
aria-label={t("space.icon.change")}
|
|
||||||
title={t("space.icon.change")}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SpaceMark icon={value} size="lg" className="size-full bg-transparent" />
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent
|
|
||||||
align="start"
|
|
||||||
className="w-[290px] max-w-[calc(100vw-2rem)] p-2"
|
|
||||||
aria-label={t("space.icon.change")}
|
|
||||||
initialFocus={searchRef}
|
|
||||||
>
|
|
||||||
<Tabs value={tab} onValueChange={changeTab}>
|
|
||||||
<TabsList className="mb-2 rounded-md bg-muted/60 p-0.5" aria-label={t("space.icon.kind")}>
|
|
||||||
{(["icons", "emoji"] as const).map((name) => (
|
|
||||||
<TabsTrigger
|
|
||||||
key={name}
|
|
||||||
id={`${id}-${name}`}
|
|
||||||
value={name}
|
|
||||||
aria-controls={`${id}-panel`}
|
|
||||||
tabIndex={tab === name ? 0 : -1}
|
|
||||||
className="flex-1 py-1 text-xs"
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
|
|
||||||
event.preventDefault();
|
|
||||||
const next = name === "icons" ? "emoji" : "icons";
|
|
||||||
changeTab(next);
|
|
||||||
document.getElementById(`${id}-${next}`)?.focus();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t(`space.icon.${name}`)}
|
|
||||||
</TabsTrigger>
|
|
||||||
))}
|
|
||||||
</TabsList>
|
|
||||||
</Tabs>
|
|
||||||
<div id={`${id}-panel`} role="tabpanel" aria-labelledby={`${id}-${tab}`}>
|
|
||||||
<div className="relative mb-2">
|
|
||||||
<SearchIcon aria-hidden className="pointer-events-none absolute start-2 top-2 size-4 text-muted-foreground" />
|
|
||||||
<Input
|
|
||||||
ref={searchRef}
|
|
||||||
value={query}
|
|
||||||
className="ps-8 text-sm"
|
|
||||||
aria-label={t(tab === "icons" ? "space.icon.search-icons" : "space.icon.search-emoji")}
|
|
||||||
placeholder={t(tab === "icons" ? "space.icon.search-icons" : "space.icon.search-emoji")}
|
|
||||||
onChange={(event) => {
|
|
||||||
setQuery(event.target.value);
|
|
||||||
setFocusedIndex(0);
|
|
||||||
}}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.nativeEvent.isComposing || event.keyCode === 229) return;
|
|
||||||
if (event.key === "Enter") {
|
|
||||||
event.preventDefault();
|
|
||||||
if (options[0]) select(options[0].icon);
|
|
||||||
} else if (event.key === "ArrowDown" && options.length) {
|
|
||||||
event.preventDefault();
|
|
||||||
setFocusedIndex(0);
|
|
||||||
gridRef.current?.querySelector<HTMLButtonElement>("button")?.focus();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="max-h-56 overflow-y-auto overscroll-contain" ref={gridRef}>
|
|
||||||
{options.length ? (
|
|
||||||
<div className="grid grid-cols-8 gap-0.5">
|
|
||||||
{options.map((option, index) => (
|
|
||||||
<Button
|
|
||||||
key={`${option.icon.value.case}:${option.icon.value.value}`}
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
aria-label={option.label}
|
|
||||||
title={option.label}
|
|
||||||
aria-pressed={spaceIconsEqual(value, option.icon)}
|
|
||||||
tabIndex={index === Math.min(focusedIndex, options.length - 1) ? 0 : -1}
|
|
||||||
onFocus={() => setFocusedIndex(index)}
|
|
||||||
onKeyDown={(event) => moveFocus(event, index)}
|
|
||||||
onClick={() => select(option.icon)}
|
|
||||||
className={cn("rounded-md", spaceIconsEqual(value, option.icon) && "bg-accent ring-1 ring-inset ring-primary/40")}
|
|
||||||
>
|
|
||||||
<SpaceIcon icon={option.icon} className={tab === "emoji" ? "size-5 text-xl" : "size-4"} />
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<p role="status" className="py-8 text-center text-xs text-muted-foreground">
|
|
||||||
{t("space.icon.no-results")}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{tab === "emoji" && <p className="px-1 pt-2 text-xs leading-4 text-muted-foreground">{t("space.icon.emoji-help")}</p>}
|
|
||||||
</div>
|
|
||||||
<div className="mt-2 border-t pt-1.5">
|
|
||||||
<Button type="button" variant="quiet" size="sm" className="w-full justify-start" disabled={!value} onClick={() => select()}>
|
|
||||||
<RotateCcwIcon aria-hidden className="size-3.5" />
|
|
||||||
{t("space.icon.reset")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default SpaceIconPicker;
|
export default SpaceIconPicker;
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import {
|
||||||
HouseIcon,
|
HouseIcon,
|
||||||
LeafIcon,
|
LeafIcon,
|
||||||
LightbulbIcon,
|
LightbulbIcon,
|
||||||
|
type LucideIcon,
|
||||||
MapPinIcon,
|
MapPinIcon,
|
||||||
MountainIcon,
|
MountainIcon,
|
||||||
MusicIcon,
|
MusicIcon,
|
||||||
|
|
@ -38,10 +39,13 @@ import {
|
||||||
WrenchIcon,
|
WrenchIcon,
|
||||||
ZapIcon,
|
ZapIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type { Space_Icon } from "@/types/proto/api/v1/space_service_pb";
|
// Shared display shape; each resource retains its own protobuf message type.
|
||||||
|
export interface CustomIconValue {
|
||||||
|
value: { case: "emoji" | "lucide"; value: string } | { case: undefined; value?: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
// Keep the catalog explicit so adding a picker does not bundle the entire icon library.
|
// Keep the catalog explicit so adding a picker does not bundle the entire icon library.
|
||||||
export const SPACE_SYMBOLS = {
|
export const CUSTOM_SYMBOLS = {
|
||||||
astroid: AstroidIcon,
|
astroid: AstroidIcon,
|
||||||
"book-open": BookOpenIcon,
|
"book-open": BookOpenIcon,
|
||||||
briefcase: BriefcaseIcon,
|
briefcase: BriefcaseIcon,
|
||||||
|
|
@ -82,7 +86,8 @@ export const SPACE_SYMBOLS = {
|
||||||
zap: ZapIcon,
|
zap: ZapIcon,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSpaceSymbol = (name: string) =>
|
export const getCustomSymbol = (name: string, fallback: LucideIcon = AstroidIcon) =>
|
||||||
Object.hasOwn(SPACE_SYMBOLS, name) ? SPACE_SYMBOLS[name as keyof typeof SPACE_SYMBOLS] : AstroidIcon;
|
Object.hasOwn(CUSTOM_SYMBOLS, name) ? CUSTOM_SYMBOLS[name as keyof typeof CUSTOM_SYMBOLS] : fallback;
|
||||||
|
|
||||||
export const spaceIconsEqual = (a?: Space_Icon, b?: Space_Icon) => a?.value.case === b?.value.case && a?.value.value === b?.value.value;
|
export const customIconsEqual = (a?: CustomIconValue, b?: CustomIconValue) =>
|
||||||
|
a?.value.case === b?.value.case && a?.value.value === b?.value.value;
|
||||||
|
|
@ -854,6 +854,7 @@
|
||||||
"uploads-privacy-title": "Yükləmələr və məxfilik"
|
"uploads-privacy-title": "Yükləmələr və məxfilik"
|
||||||
},
|
},
|
||||||
"memo-view": {
|
"memo-view": {
|
||||||
|
"change-icon": "Görünüş ikonunu dəyiş",
|
||||||
"delete-confirm": "`{{title}}` görünüşünü silmək istədiyinizə əminsiniz?",
|
"delete-confirm": "`{{title}}` görünüşünü silmək istədiyinizə əminsiniz?",
|
||||||
"delete-success": "`{{title}}` görünüşü silindi"
|
"delete-success": "`{{title}}` görünüşü silindi"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -854,6 +854,7 @@
|
||||||
"uploads-privacy-title": "Uploads & privacy"
|
"uploads-privacy-title": "Uploads & privacy"
|
||||||
},
|
},
|
||||||
"memo-view": {
|
"memo-view": {
|
||||||
|
"change-icon": "Change view icon",
|
||||||
"delete-confirm": "Are you sure you want to delete view `{{title}}`?",
|
"delete-confirm": "Are you sure you want to delete view `{{title}}`?",
|
||||||
"delete-success": "View `{{title}}` deleted successfully"
|
"delete-success": "View `{{title}}` deleted successfully"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import {
|
||||||
FilterIcon,
|
FilterIcon,
|
||||||
MapPinIcon,
|
MapPinIcon,
|
||||||
MoreVerticalIcon,
|
MoreVerticalIcon,
|
||||||
|
ParenthesesIcon,
|
||||||
PencilIcon,
|
PencilIcon,
|
||||||
PinIcon,
|
PinIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
|
|
@ -24,6 +25,8 @@ import { useEffect, useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import ConfirmDialog from "@/components/ConfirmDialog";
|
import ConfirmDialog from "@/components/ConfirmDialog";
|
||||||
|
import CustomIconPicker from "@/components/CustomIconPicker";
|
||||||
|
import MemoViewIcon from "@/components/MemoViewIcon";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||||
|
|
@ -38,7 +41,7 @@ import { useMemoViews, userKeys } from "@/hooks/useUserQueries";
|
||||||
import { handleError } from "@/lib/error";
|
import { handleError } from "@/lib/error";
|
||||||
import { getMemoViewId } from "@/lib/memo-views";
|
import { getMemoViewId } from "@/lib/memo-views";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { MemoView, MemoViewSchema } from "@/types/proto/api/v1/user_service_pb";
|
import { MemoView, MemoView_IconSchema, MemoViewSchema } from "@/types/proto/api/v1/user_service_pb";
|
||||||
import { useTranslate } from "@/utils/i18n";
|
import { useTranslate } from "@/utils/i18n";
|
||||||
|
|
||||||
const memoViewExamples = [
|
const memoViewExamples = [
|
||||||
|
|
@ -287,6 +290,7 @@ const MemoViews = () => {
|
||||||
name: state.memoView.name,
|
name: state.memoView.name,
|
||||||
title: state.memoView.title,
|
title: state.memoView.title,
|
||||||
filter: state.memoView.filter,
|
filter: state.memoView.filter,
|
||||||
|
icon: state.memoView.icon,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
setIsCreateFormOpen(true);
|
setIsCreateFormOpen(true);
|
||||||
|
|
@ -308,6 +312,7 @@ const MemoViews = () => {
|
||||||
name: draft.name,
|
name: draft.name,
|
||||||
title: draft.title || example.title,
|
title: draft.title || example.title,
|
||||||
filter: example.filter,
|
filter: example.filter,
|
||||||
|
icon: draft.icon,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
setIsCreateFormOpen(true);
|
setIsCreateFormOpen(true);
|
||||||
|
|
@ -329,6 +334,7 @@ const MemoViews = () => {
|
||||||
name: memoView.name,
|
name: memoView.name,
|
||||||
title: memoView.title,
|
title: memoView.title,
|
||||||
filter: memoView.filter,
|
filter: memoView.filter,
|
||||||
|
icon: memoView.icon,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
setIsCreateFormOpen(true);
|
setIsCreateFormOpen(true);
|
||||||
|
|
@ -348,7 +354,7 @@ const MemoViews = () => {
|
||||||
validateState.setLoading();
|
validateState.setLoading();
|
||||||
await userServiceClient.createMemoView({
|
await userServiceClient.createMemoView({
|
||||||
parent: user.name,
|
parent: user.name,
|
||||||
memoView: { name: "", title: draft.title, filter: draft.filter },
|
memoView: { name: "", title: draft.title, filter: draft.filter, icon: draft.icon },
|
||||||
validateOnly: true,
|
validateOnly: true,
|
||||||
});
|
});
|
||||||
validateState.setFinish();
|
validateState.setFinish();
|
||||||
|
|
@ -377,7 +383,7 @@ const MemoViews = () => {
|
||||||
createState.setLoading();
|
createState.setLoading();
|
||||||
await userServiceClient.createMemoView({
|
await userServiceClient.createMemoView({
|
||||||
parent: user.name,
|
parent: user.name,
|
||||||
memoView: { name: "", title: draft.title, filter: draft.filter },
|
memoView: { name: "", title: draft.title, filter: draft.filter, icon: draft.icon },
|
||||||
});
|
});
|
||||||
await queryClient.invalidateQueries({ queryKey: userKeys.memoViews(user.name) });
|
await queryClient.invalidateQueries({ queryKey: userKeys.memoViews(user.name) });
|
||||||
createState.setFinish();
|
createState.setFinish();
|
||||||
|
|
@ -402,7 +408,7 @@ const MemoViews = () => {
|
||||||
updateState.setLoading();
|
updateState.setLoading();
|
||||||
await userServiceClient.updateMemoView({
|
await userServiceClient.updateMemoView({
|
||||||
memoView: draft,
|
memoView: draft,
|
||||||
updateMask: create(FieldMaskSchema, { paths: ["title", "filter"] }),
|
updateMask: create(FieldMaskSchema, { paths: ["title", "filter", "icon"] }),
|
||||||
});
|
});
|
||||||
await queryClient.invalidateQueries({ queryKey: userKeys.memoViews(user?.name) });
|
await queryClient.invalidateQueries({ queryKey: userKeys.memoViews(user?.name) });
|
||||||
updateState.setFinish();
|
updateState.setFinish();
|
||||||
|
|
@ -491,15 +497,21 @@ const MemoViews = () => {
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor="view-title">{t("common.title")}</Label>
|
<Label htmlFor="view-title">{t("common.title")}</Label>
|
||||||
<Input
|
<div className="flex items-center gap-2">
|
||||||
id="view-title"
|
<CustomIconPicker
|
||||||
value={draft.title}
|
value={draft.icon}
|
||||||
placeholder="Pinned, Recent notes, Work"
|
onChange={(icon) => setDraftState({ icon: icon ? create(MemoView_IconSchema, icon) : undefined })}
|
||||||
onChange={(event) => setDraftState({ title: event.target.value })}
|
label={t("setting.memo-view.change-icon")}
|
||||||
/>
|
fallback={ParenthesesIcon}
|
||||||
<p className="text-xs leading-5 text-muted-foreground">
|
disabled={isSaving}
|
||||||
Prefix the title with an emoji if you want it to appear in the sidebar.
|
/>
|
||||||
</p>
|
<Input
|
||||||
|
id="view-title"
|
||||||
|
value={draft.title}
|
||||||
|
placeholder="Pinned, Recent notes, Work"
|
||||||
|
onChange={(event) => setDraftState({ title: event.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor="view-filter">{t("common.filter")}</Label>
|
<Label htmlFor="view-filter">{t("common.filter")}</Label>
|
||||||
|
|
@ -554,7 +566,10 @@ const MemoViews = () => {
|
||||||
className="grid gap-3 bg-background px-4 py-3 sm:grid-cols-[minmax(10rem,14rem)_minmax(0,1fr)_2rem]"
|
className="grid gap-3 bg-background px-4 py-3 sm:grid-cols-[minmax(10rem,14rem)_minmax(0,1fr)_2rem]"
|
||||||
>
|
>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="truncate text-sm font-medium text-foreground">{memoView.title}</div>
|
<div className="flex items-center gap-2 text-sm font-medium text-foreground">
|
||||||
|
<MemoViewIcon icon={memoView.icon} className="size-4 text-base" />
|
||||||
|
<span className="truncate">{memoView.title}</span>
|
||||||
|
</div>
|
||||||
<div className="mt-1 font-mono text-xs text-muted-foreground">{getMemoViewId(memoView.name)}</div>
|
<div className="mt-1 font-mono text-xs text-muted-foreground">{getMemoViewId(memoView.name)}</div>
|
||||||
</div>
|
</div>
|
||||||
<pre className="min-w-0 overflow-x-auto rounded-md bg-muted/50 px-3 py-2 font-mono text-xs leading-5 text-muted-foreground">
|
<pre className="min-w-0 overflow-x-auto rounded-md bg-muted/50 px-3 py-2 font-mono text-xs leading-5 text-muted-foreground">
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
112
web/tests/memo-view-icons.test.tsx
Normal file
112
web/tests/memo-view-icons.test.tsx
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
import { create } from "@bufbuild/protobuf";
|
||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||||
|
import { MemoryRouter } from "react-router-dom";
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import MemoViewIcon from "@/components/MemoViewIcon";
|
||||||
|
import MemoViews from "@/pages/MemoViews";
|
||||||
|
import { MemoView_IconSchema, MemoViewSchema } from "@/types/proto/api/v1/user_service_pb";
|
||||||
|
|
||||||
|
const mocks = vi.hoisted(() => ({ create: vi.fn(), update: vi.fn() }));
|
||||||
|
vi.mock("@/connect", () => ({ userServiceClient: { createMemoView: mocks.create, updateMemoView: mocks.update } }));
|
||||||
|
vi.mock("@/hooks/useCurrentUser", () => ({ default: () => ({ name: "users/steven" }) }));
|
||||||
|
vi.mock("@/hooks/useUserQueries", () => ({ useMemoViews: () => ({ data: [] }), userKeys: { memoViews: () => ["views"] } }));
|
||||||
|
vi.mock("@/contexts/MemoFilterContext", () => ({ useMemoFilterContext: () => ({ setMemoView: vi.fn() }) }));
|
||||||
|
vi.mock("@/utils/i18n", () => ({ useTranslate: () => (key: string) => key }));
|
||||||
|
|
||||||
|
const emoji = create(MemoView_IconSchema, { value: { case: "emoji", value: "🌱" } });
|
||||||
|
const leaf = create(MemoView_IconSchema, { value: { case: "lucide", value: "leaf" } });
|
||||||
|
const existingView = create(MemoViewSchema, { name: "users/steven/views/garden", title: "Garden", filter: "pinned", icon: emoji });
|
||||||
|
const iconMask = expect.objectContaining({ paths: ["title", "filter", "icon"] });
|
||||||
|
|
||||||
|
function renderPage(edit = false) {
|
||||||
|
return render(
|
||||||
|
<QueryClientProvider client={new QueryClient({ defaultOptions: { queries: { retry: false } } })}>
|
||||||
|
<MemoryRouter initialEntries={[{ pathname: "/views", state: edit ? { memoView: existingView } : { openCreate: true } }]}>
|
||||||
|
<MemoViews />
|
||||||
|
</MemoryRouter>
|
||||||
|
</QueryClientProvider>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
mocks.create.mockResolvedValue({});
|
||||||
|
mocks.update.mockResolvedValue({});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("memo view icons", () => {
|
||||||
|
it("renders emoji and symbols with a view-specific fallback", () => {
|
||||||
|
const view = render(<MemoViewIcon />);
|
||||||
|
expect(view.container.querySelector(".lucide-parentheses")).not.toBeNull();
|
||||||
|
view.rerender(<MemoViewIcon icon={emoji} />);
|
||||||
|
expect(view.container).toHaveTextContent("🌱");
|
||||||
|
view.rerender(<MemoViewIcon icon={leaf} />);
|
||||||
|
expect(view.container.querySelector(".lucide-leaf")).not.toBeNull();
|
||||||
|
view.rerender(<MemoViewIcon icon={create(MemoView_IconSchema, { value: { case: "lucide", value: "future-icon" } })} />);
|
||||||
|
expect(view.container.querySelector(".lucide-parentheses")).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates and validates a view with a selected icon", async () => {
|
||||||
|
renderPage();
|
||||||
|
fireEvent.change(screen.getByLabelText("common.title"), { target: { value: "Garden" } });
|
||||||
|
fireEvent.change(screen.getByLabelText("common.filter"), { target: { value: "pinned" } });
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "setting.memo-view.change-icon" }));
|
||||||
|
const search = await screen.findByRole("textbox", { name: "space.icon.search-icons" });
|
||||||
|
fireEvent.change(search, { target: { value: "leaf" } });
|
||||||
|
fireEvent.keyDown(search, { key: "Enter" });
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Validate" }));
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(mocks.create).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
validateOnly: true,
|
||||||
|
memoView: expect.objectContaining({ icon: leaf }),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await waitFor(() => expect(screen.getByRole("button", { name: "common.save" })).toBeEnabled());
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "common.save" }));
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(mocks.create).toHaveBeenLastCalledWith({
|
||||||
|
parent: "users/steven",
|
||||||
|
memoView: { name: "", title: "Garden", filter: "pinned", icon: leaf },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves the icon when editing from the sidebar and applying an example", async () => {
|
||||||
|
renderPage(true);
|
||||||
|
expect(screen.getByRole("button", { name: "setting.memo-view.change-icon" })).toHaveTextContent("🌱");
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /Recent notes/ }));
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "common.save" }));
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(mocks.update).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
memoView: expect.objectContaining({
|
||||||
|
name: existingView.name,
|
||||||
|
title: "Garden",
|
||||||
|
icon: emoji,
|
||||||
|
filter: 'created_ts >= now - duration("1h")',
|
||||||
|
}),
|
||||||
|
updateMask: iconMask,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resets an existing icon using the icon update mask", async () => {
|
||||||
|
renderPage(true);
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "setting.memo-view.change-icon" }));
|
||||||
|
fireEvent.click(await screen.findByRole("button", { name: "space.icon.reset" }));
|
||||||
|
expect(screen.getByRole("button", { name: "setting.memo-view.change-icon" }).querySelector(".lucide-parentheses")).not.toBeNull();
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "common.save" }));
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(mocks.update).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
memoView: expect.objectContaining({ icon: undefined }),
|
||||||
|
updateMask: iconMask,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue