mirror of https://github.com/usememos/memos.git
feat(user): add per-user tag metadata settings (#5735)
This commit is contained in:
parent
04f239a2fc
commit
330291d4d9
5
go.mod
5
go.mod
|
|
@ -32,7 +32,8 @@ require (
|
|||
golang.org/x/net v0.52.0
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
golang.org/x/sync v0.20.0
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c
|
||||
google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260316172706-e463d84ca32d
|
||||
google.golang.org/grpc v1.79.2
|
||||
modernc.org/sqlite v1.46.1
|
||||
)
|
||||
|
|
@ -106,7 +107,7 @@ require (
|
|||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
||||
golang.org/x/image v0.30.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect
|
||||
modernc.org/libc v1.67.6 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
|
|
|
|||
10
go.sum
10
go.sum
|
|
@ -307,10 +307,12 @@ golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0
|
|||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c h1:OyQPd6I3pN/9gDxz6L13kYGJgqkpdrAohJRBeXyxlgI=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c/go.mod h1:X2gu9Qwng7Nn009s/r3RUxqkzQNqOrAy79bluY7ojIg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5 h1:JNfk58HZ8lfmXbYK2vx/UvsqIL59TzByCxPIX4TDmsE=
|
||||
google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:x5julN69+ED4PcFk/XWayw35O0lf/nGa4aNgODCmNmw=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260316172706-e463d84ca32d h1:RdWlPmVySdTF0IBIZzvZJvSD0ZocPBNUsnE+uGBxj+4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260316172706-e463d84ca32d/go.mod h1:X2gu9Qwng7Nn009s/r3RUxqkzQNqOrAy79bluY7ojIg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
|
||||
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import "google/api/resource.proto";
|
|||
import "google/protobuf/empty.proto";
|
||||
import "google/protobuf/field_mask.proto";
|
||||
import "google/protobuf/timestamp.proto";
|
||||
import "google/type/color.proto";
|
||||
|
||||
option go_package = "gen/api/v1";
|
||||
|
||||
|
|
@ -375,6 +376,7 @@ message UserSetting {
|
|||
oneof value {
|
||||
GeneralSetting general_setting = 2;
|
||||
WebhooksSetting webhooks_setting = 5;
|
||||
TagsSetting tags_setting = 6;
|
||||
}
|
||||
|
||||
// Enumeration of user setting keys.
|
||||
|
|
@ -384,6 +386,8 @@ message UserSetting {
|
|||
GENERAL = 1;
|
||||
// WEBHOOKS is the key for user webhooks.
|
||||
WEBHOOKS = 4;
|
||||
// TAGS is the key for user tag metadata.
|
||||
TAGS = 5;
|
||||
}
|
||||
|
||||
// General user settings configuration.
|
||||
|
|
@ -403,6 +407,17 @@ message UserSetting {
|
|||
// List of user webhooks.
|
||||
repeated UserWebhook webhooks = 1;
|
||||
}
|
||||
|
||||
// Metadata for a tag.
|
||||
message TagMetadata {
|
||||
// Background color for the tag label.
|
||||
google.type.Color background_color = 1;
|
||||
}
|
||||
|
||||
// User tag metadata configuration.
|
||||
message TagsSetting {
|
||||
map<string, TagMetadata> tags = 1;
|
||||
}
|
||||
}
|
||||
|
||||
message GetUserSettingRequest {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ package apiv1
|
|||
|
||||
import (
|
||||
_ "google.golang.org/genproto/googleapis/api/annotations"
|
||||
color "google.golang.org/genproto/googleapis/type/color"
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||
|
|
@ -86,6 +87,8 @@ const (
|
|||
UserSetting_GENERAL UserSetting_Key = 1
|
||||
// WEBHOOKS is the key for user webhooks.
|
||||
UserSetting_WEBHOOKS UserSetting_Key = 4
|
||||
// TAGS is the key for user tag metadata.
|
||||
UserSetting_TAGS UserSetting_Key = 5
|
||||
)
|
||||
|
||||
// Enum value maps for UserSetting_Key.
|
||||
|
|
@ -94,11 +97,13 @@ var (
|
|||
0: "KEY_UNSPECIFIED",
|
||||
1: "GENERAL",
|
||||
4: "WEBHOOKS",
|
||||
5: "TAGS",
|
||||
}
|
||||
UserSetting_Key_value = map[string]int32{
|
||||
"KEY_UNSPECIFIED": 0,
|
||||
"GENERAL": 1,
|
||||
"WEBHOOKS": 4,
|
||||
"TAGS": 5,
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -986,6 +991,7 @@ type UserSetting struct {
|
|||
//
|
||||
// *UserSetting_GeneralSetting_
|
||||
// *UserSetting_WebhooksSetting_
|
||||
// *UserSetting_TagsSetting_
|
||||
Value isUserSetting_Value `protobuf_oneof:"value"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
|
|
@ -1053,6 +1059,15 @@ func (x *UserSetting) GetWebhooksSetting() *UserSetting_WebhooksSetting {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (x *UserSetting) GetTagsSetting() *UserSetting_TagsSetting {
|
||||
if x != nil {
|
||||
if x, ok := x.Value.(*UserSetting_TagsSetting_); ok {
|
||||
return x.TagsSetting
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type isUserSetting_Value interface {
|
||||
isUserSetting_Value()
|
||||
}
|
||||
|
|
@ -1065,10 +1080,16 @@ type UserSetting_WebhooksSetting_ struct {
|
|||
WebhooksSetting *UserSetting_WebhooksSetting `protobuf:"bytes,5,opt,name=webhooks_setting,json=webhooksSetting,proto3,oneof"`
|
||||
}
|
||||
|
||||
type UserSetting_TagsSetting_ struct {
|
||||
TagsSetting *UserSetting_TagsSetting `protobuf:"bytes,6,opt,name=tags_setting,json=tagsSetting,proto3,oneof"`
|
||||
}
|
||||
|
||||
func (*UserSetting_GeneralSetting_) isUserSetting_Value() {}
|
||||
|
||||
func (*UserSetting_WebhooksSetting_) isUserSetting_Value() {}
|
||||
|
||||
func (*UserSetting_TagsSetting_) isUserSetting_Value() {}
|
||||
|
||||
type GetUserSettingRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// Required. The resource name of the user setting.
|
||||
|
|
@ -2521,6 +2542,97 @@ func (x *UserSetting_WebhooksSetting) GetWebhooks() []*UserWebhook {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Metadata for a tag.
|
||||
type UserSetting_TagMetadata struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// Background color for the tag label.
|
||||
BackgroundColor *color.Color `protobuf:"bytes,1,opt,name=background_color,json=backgroundColor,proto3" json:"background_color,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *UserSetting_TagMetadata) Reset() {
|
||||
*x = UserSetting_TagMetadata{}
|
||||
mi := &file_api_v1_user_service_proto_msgTypes[37]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *UserSetting_TagMetadata) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*UserSetting_TagMetadata) ProtoMessage() {}
|
||||
|
||||
func (x *UserSetting_TagMetadata) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_api_v1_user_service_proto_msgTypes[37]
|
||||
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 UserSetting_TagMetadata.ProtoReflect.Descriptor instead.
|
||||
func (*UserSetting_TagMetadata) Descriptor() ([]byte, []int) {
|
||||
return file_api_v1_user_service_proto_rawDescGZIP(), []int{11, 2}
|
||||
}
|
||||
|
||||
func (x *UserSetting_TagMetadata) GetBackgroundColor() *color.Color {
|
||||
if x != nil {
|
||||
return x.BackgroundColor
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// User tag metadata configuration.
|
||||
type UserSetting_TagsSetting struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Tags map[string]*UserSetting_TagMetadata `protobuf:"bytes,1,rep,name=tags,proto3" json:"tags,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *UserSetting_TagsSetting) Reset() {
|
||||
*x = UserSetting_TagsSetting{}
|
||||
mi := &file_api_v1_user_service_proto_msgTypes[38]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *UserSetting_TagsSetting) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*UserSetting_TagsSetting) ProtoMessage() {}
|
||||
|
||||
func (x *UserSetting_TagsSetting) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_api_v1_user_service_proto_msgTypes[38]
|
||||
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 UserSetting_TagsSetting.ProtoReflect.Descriptor instead.
|
||||
func (*UserSetting_TagsSetting) Descriptor() ([]byte, []int) {
|
||||
return file_api_v1_user_service_proto_rawDescGZIP(), []int{11, 3}
|
||||
}
|
||||
|
||||
func (x *UserSetting_TagsSetting) GetTags() map[string]*UserSetting_TagMetadata {
|
||||
if x != nil {
|
||||
return x.Tags
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type UserNotification_MemoCommentPayload struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// The memo name of comment.
|
||||
|
|
@ -2535,7 +2647,7 @@ type UserNotification_MemoCommentPayload struct {
|
|||
|
||||
func (x *UserNotification_MemoCommentPayload) Reset() {
|
||||
*x = UserNotification_MemoCommentPayload{}
|
||||
mi := &file_api_v1_user_service_proto_msgTypes[37]
|
||||
mi := &file_api_v1_user_service_proto_msgTypes[40]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
|
@ -2547,7 +2659,7 @@ func (x *UserNotification_MemoCommentPayload) String() string {
|
|||
func (*UserNotification_MemoCommentPayload) ProtoMessage() {}
|
||||
|
||||
func (x *UserNotification_MemoCommentPayload) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_api_v1_user_service_proto_msgTypes[37]
|
||||
mi := &file_api_v1_user_service_proto_msgTypes[40]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
|
|
@ -2581,7 +2693,7 @@ var File_api_v1_user_service_proto protoreflect.FileDescriptor
|
|||
|
||||
const file_api_v1_user_service_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x19api/v1/user_service.proto\x12\fmemos.api.v1\x1a\x13api/v1/common.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x17google/api/client.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/resource.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a google/protobuf/field_mask.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xc1\x04\n" +
|
||||
"\x19api/v1/user_service.proto\x12\fmemos.api.v1\x1a\x13api/v1/common.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x17google/api/client.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/resource.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a google/protobuf/field_mask.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x17google/type/color.proto\"\xc1\x04\n" +
|
||||
"\x04User\x12\x17\n" +
|
||||
"\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x120\n" +
|
||||
"\x04role\x18\x02 \x01(\x0e2\x17.memos.api.v1.User.RoleB\x03\xe0A\x02R\x04role\x12\x1f\n" +
|
||||
|
|
@ -2658,21 +2770,30 @@ const file_api_v1_user_service_proto_rawDesc = "" +
|
|||
"\x11memos.api.v1/UserR\x04name\"\x19\n" +
|
||||
"\x17ListAllUserStatsRequest\"I\n" +
|
||||
"\x18ListAllUserStatsResponse\x12-\n" +
|
||||
"\x05stats\x18\x01 \x03(\v2\x17.memos.api.v1.UserStatsR\x05stats\"\xb0\x04\n" +
|
||||
"\x05stats\x18\x01 \x03(\v2\x17.memos.api.v1.UserStatsR\x05stats\"\x89\a\n" +
|
||||
"\vUserSetting\x12\x17\n" +
|
||||
"\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12S\n" +
|
||||
"\x0fgeneral_setting\x18\x02 \x01(\v2(.memos.api.v1.UserSetting.GeneralSettingH\x00R\x0egeneralSetting\x12V\n" +
|
||||
"\x10webhooks_setting\x18\x05 \x01(\v2).memos.api.v1.UserSetting.WebhooksSettingH\x00R\x0fwebhooksSetting\x1av\n" +
|
||||
"\x10webhooks_setting\x18\x05 \x01(\v2).memos.api.v1.UserSetting.WebhooksSettingH\x00R\x0fwebhooksSetting\x12J\n" +
|
||||
"\ftags_setting\x18\x06 \x01(\v2%.memos.api.v1.UserSetting.TagsSettingH\x00R\vtagsSetting\x1av\n" +
|
||||
"\x0eGeneralSetting\x12\x1b\n" +
|
||||
"\x06locale\x18\x01 \x01(\tB\x03\xe0A\x01R\x06locale\x12,\n" +
|
||||
"\x0fmemo_visibility\x18\x03 \x01(\tB\x03\xe0A\x01R\x0ememoVisibility\x12\x19\n" +
|
||||
"\x05theme\x18\x04 \x01(\tB\x03\xe0A\x01R\x05theme\x1aH\n" +
|
||||
"\x0fWebhooksSetting\x125\n" +
|
||||
"\bwebhooks\x18\x01 \x03(\v2\x19.memos.api.v1.UserWebhookR\bwebhooks\"5\n" +
|
||||
"\bwebhooks\x18\x01 \x03(\v2\x19.memos.api.v1.UserWebhookR\bwebhooks\x1aL\n" +
|
||||
"\vTagMetadata\x12=\n" +
|
||||
"\x10background_color\x18\x01 \x01(\v2\x12.google.type.ColorR\x0fbackgroundColor\x1a\xb2\x01\n" +
|
||||
"\vTagsSetting\x12C\n" +
|
||||
"\x04tags\x18\x01 \x03(\v2/.memos.api.v1.UserSetting.TagsSetting.TagsEntryR\x04tags\x1a^\n" +
|
||||
"\tTagsEntry\x12\x10\n" +
|
||||
"\x03key\x18\x01 \x01(\tR\x03key\x12;\n" +
|
||||
"\x05value\x18\x02 \x01(\v2%.memos.api.v1.UserSetting.TagMetadataR\x05value:\x028\x01\"?\n" +
|
||||
"\x03Key\x12\x13\n" +
|
||||
"\x0fKEY_UNSPECIFIED\x10\x00\x12\v\n" +
|
||||
"\aGENERAL\x10\x01\x12\f\n" +
|
||||
"\bWEBHOOKS\x10\x04:Y\xeaAV\n" +
|
||||
"\bWEBHOOKS\x10\x04\x12\b\n" +
|
||||
"\x04TAGS\x10\x05:Y\xeaAV\n" +
|
||||
"\x18memos.api.v1/UserSetting\x12\x1fusers/{user}/settings/{setting}*\fuserSettings2\vuserSettingB\a\n" +
|
||||
"\x05value\"M\n" +
|
||||
"\x15GetUserSettingRequest\x124\n" +
|
||||
|
|
@ -2824,7 +2945,7 @@ func file_api_v1_user_service_proto_rawDescGZIP() []byte {
|
|||
}
|
||||
|
||||
var file_api_v1_user_service_proto_enumTypes = make([]protoimpl.EnumInfo, 4)
|
||||
var file_api_v1_user_service_proto_msgTypes = make([]protoimpl.MessageInfo, 38)
|
||||
var file_api_v1_user_service_proto_msgTypes = make([]protoimpl.MessageInfo, 41)
|
||||
var file_api_v1_user_service_proto_goTypes = []any{
|
||||
(User_Role)(0), // 0: memos.api.v1.User.Role
|
||||
(UserSetting_Key)(0), // 1: memos.api.v1.UserSetting.Key
|
||||
|
|
@ -2867,95 +2988,103 @@ var file_api_v1_user_service_proto_goTypes = []any{
|
|||
(*UserStats_MemoTypeStats)(nil), // 38: memos.api.v1.UserStats.MemoTypeStats
|
||||
(*UserSetting_GeneralSetting)(nil), // 39: memos.api.v1.UserSetting.GeneralSetting
|
||||
(*UserSetting_WebhooksSetting)(nil), // 40: memos.api.v1.UserSetting.WebhooksSetting
|
||||
(*UserNotification_MemoCommentPayload)(nil), // 41: memos.api.v1.UserNotification.MemoCommentPayload
|
||||
(State)(0), // 42: memos.api.v1.State
|
||||
(*timestamppb.Timestamp)(nil), // 43: google.protobuf.Timestamp
|
||||
(*fieldmaskpb.FieldMask)(nil), // 44: google.protobuf.FieldMask
|
||||
(*emptypb.Empty)(nil), // 45: google.protobuf.Empty
|
||||
(*UserSetting_TagMetadata)(nil), // 41: memos.api.v1.UserSetting.TagMetadata
|
||||
(*UserSetting_TagsSetting)(nil), // 42: memos.api.v1.UserSetting.TagsSetting
|
||||
nil, // 43: memos.api.v1.UserSetting.TagsSetting.TagsEntry
|
||||
(*UserNotification_MemoCommentPayload)(nil), // 44: memos.api.v1.UserNotification.MemoCommentPayload
|
||||
(State)(0), // 45: memos.api.v1.State
|
||||
(*timestamppb.Timestamp)(nil), // 46: google.protobuf.Timestamp
|
||||
(*fieldmaskpb.FieldMask)(nil), // 47: google.protobuf.FieldMask
|
||||
(*color.Color)(nil), // 48: google.type.Color
|
||||
(*emptypb.Empty)(nil), // 49: google.protobuf.Empty
|
||||
}
|
||||
var file_api_v1_user_service_proto_depIdxs = []int32{
|
||||
0, // 0: memos.api.v1.User.role:type_name -> memos.api.v1.User.Role
|
||||
42, // 1: memos.api.v1.User.state:type_name -> memos.api.v1.State
|
||||
43, // 2: memos.api.v1.User.create_time:type_name -> google.protobuf.Timestamp
|
||||
43, // 3: memos.api.v1.User.update_time:type_name -> google.protobuf.Timestamp
|
||||
45, // 1: memos.api.v1.User.state:type_name -> memos.api.v1.State
|
||||
46, // 2: memos.api.v1.User.create_time:type_name -> google.protobuf.Timestamp
|
||||
46, // 3: memos.api.v1.User.update_time:type_name -> google.protobuf.Timestamp
|
||||
4, // 4: memos.api.v1.ListUsersResponse.users:type_name -> memos.api.v1.User
|
||||
44, // 5: memos.api.v1.GetUserRequest.read_mask:type_name -> google.protobuf.FieldMask
|
||||
47, // 5: memos.api.v1.GetUserRequest.read_mask:type_name -> google.protobuf.FieldMask
|
||||
4, // 6: memos.api.v1.CreateUserRequest.user:type_name -> memos.api.v1.User
|
||||
4, // 7: memos.api.v1.UpdateUserRequest.user:type_name -> memos.api.v1.User
|
||||
44, // 8: memos.api.v1.UpdateUserRequest.update_mask:type_name -> google.protobuf.FieldMask
|
||||
43, // 9: memos.api.v1.UserStats.memo_display_timestamps:type_name -> google.protobuf.Timestamp
|
||||
47, // 8: memos.api.v1.UpdateUserRequest.update_mask:type_name -> google.protobuf.FieldMask
|
||||
46, // 9: memos.api.v1.UserStats.memo_display_timestamps:type_name -> google.protobuf.Timestamp
|
||||
38, // 10: memos.api.v1.UserStats.memo_type_stats:type_name -> memos.api.v1.UserStats.MemoTypeStats
|
||||
37, // 11: memos.api.v1.UserStats.tag_count:type_name -> memos.api.v1.UserStats.TagCountEntry
|
||||
11, // 12: memos.api.v1.ListAllUserStatsResponse.stats:type_name -> memos.api.v1.UserStats
|
||||
39, // 13: memos.api.v1.UserSetting.general_setting:type_name -> memos.api.v1.UserSetting.GeneralSetting
|
||||
40, // 14: memos.api.v1.UserSetting.webhooks_setting:type_name -> memos.api.v1.UserSetting.WebhooksSetting
|
||||
15, // 15: memos.api.v1.UpdateUserSettingRequest.setting:type_name -> memos.api.v1.UserSetting
|
||||
44, // 16: memos.api.v1.UpdateUserSettingRequest.update_mask:type_name -> google.protobuf.FieldMask
|
||||
15, // 17: memos.api.v1.ListUserSettingsResponse.settings:type_name -> memos.api.v1.UserSetting
|
||||
43, // 18: memos.api.v1.PersonalAccessToken.created_at:type_name -> google.protobuf.Timestamp
|
||||
43, // 19: memos.api.v1.PersonalAccessToken.expires_at:type_name -> google.protobuf.Timestamp
|
||||
43, // 20: memos.api.v1.PersonalAccessToken.last_used_at:type_name -> google.protobuf.Timestamp
|
||||
20, // 21: memos.api.v1.ListPersonalAccessTokensResponse.personal_access_tokens:type_name -> memos.api.v1.PersonalAccessToken
|
||||
20, // 22: memos.api.v1.CreatePersonalAccessTokenResponse.personal_access_token:type_name -> memos.api.v1.PersonalAccessToken
|
||||
43, // 23: memos.api.v1.UserWebhook.create_time:type_name -> google.protobuf.Timestamp
|
||||
43, // 24: memos.api.v1.UserWebhook.update_time:type_name -> google.protobuf.Timestamp
|
||||
26, // 25: memos.api.v1.ListUserWebhooksResponse.webhooks:type_name -> memos.api.v1.UserWebhook
|
||||
26, // 26: memos.api.v1.CreateUserWebhookRequest.webhook:type_name -> memos.api.v1.UserWebhook
|
||||
26, // 27: memos.api.v1.UpdateUserWebhookRequest.webhook:type_name -> memos.api.v1.UserWebhook
|
||||
44, // 28: memos.api.v1.UpdateUserWebhookRequest.update_mask:type_name -> google.protobuf.FieldMask
|
||||
2, // 29: memos.api.v1.UserNotification.status:type_name -> memos.api.v1.UserNotification.Status
|
||||
43, // 30: memos.api.v1.UserNotification.create_time:type_name -> google.protobuf.Timestamp
|
||||
3, // 31: memos.api.v1.UserNotification.type:type_name -> memos.api.v1.UserNotification.Type
|
||||
41, // 32: memos.api.v1.UserNotification.memo_comment:type_name -> memos.api.v1.UserNotification.MemoCommentPayload
|
||||
32, // 33: memos.api.v1.ListUserNotificationsResponse.notifications:type_name -> memos.api.v1.UserNotification
|
||||
32, // 34: memos.api.v1.UpdateUserNotificationRequest.notification:type_name -> memos.api.v1.UserNotification
|
||||
44, // 35: memos.api.v1.UpdateUserNotificationRequest.update_mask:type_name -> google.protobuf.FieldMask
|
||||
26, // 36: memos.api.v1.UserSetting.WebhooksSetting.webhooks:type_name -> memos.api.v1.UserWebhook
|
||||
5, // 37: memos.api.v1.UserService.ListUsers:input_type -> memos.api.v1.ListUsersRequest
|
||||
7, // 38: memos.api.v1.UserService.GetUser:input_type -> memos.api.v1.GetUserRequest
|
||||
8, // 39: memos.api.v1.UserService.CreateUser:input_type -> memos.api.v1.CreateUserRequest
|
||||
9, // 40: memos.api.v1.UserService.UpdateUser:input_type -> memos.api.v1.UpdateUserRequest
|
||||
10, // 41: memos.api.v1.UserService.DeleteUser:input_type -> memos.api.v1.DeleteUserRequest
|
||||
13, // 42: memos.api.v1.UserService.ListAllUserStats:input_type -> memos.api.v1.ListAllUserStatsRequest
|
||||
12, // 43: memos.api.v1.UserService.GetUserStats:input_type -> memos.api.v1.GetUserStatsRequest
|
||||
16, // 44: memos.api.v1.UserService.GetUserSetting:input_type -> memos.api.v1.GetUserSettingRequest
|
||||
17, // 45: memos.api.v1.UserService.UpdateUserSetting:input_type -> memos.api.v1.UpdateUserSettingRequest
|
||||
18, // 46: memos.api.v1.UserService.ListUserSettings:input_type -> memos.api.v1.ListUserSettingsRequest
|
||||
21, // 47: memos.api.v1.UserService.ListPersonalAccessTokens:input_type -> memos.api.v1.ListPersonalAccessTokensRequest
|
||||
23, // 48: memos.api.v1.UserService.CreatePersonalAccessToken:input_type -> memos.api.v1.CreatePersonalAccessTokenRequest
|
||||
25, // 49: memos.api.v1.UserService.DeletePersonalAccessToken:input_type -> memos.api.v1.DeletePersonalAccessTokenRequest
|
||||
27, // 50: memos.api.v1.UserService.ListUserWebhooks:input_type -> memos.api.v1.ListUserWebhooksRequest
|
||||
29, // 51: memos.api.v1.UserService.CreateUserWebhook:input_type -> memos.api.v1.CreateUserWebhookRequest
|
||||
30, // 52: memos.api.v1.UserService.UpdateUserWebhook:input_type -> memos.api.v1.UpdateUserWebhookRequest
|
||||
31, // 53: memos.api.v1.UserService.DeleteUserWebhook:input_type -> memos.api.v1.DeleteUserWebhookRequest
|
||||
33, // 54: memos.api.v1.UserService.ListUserNotifications:input_type -> memos.api.v1.ListUserNotificationsRequest
|
||||
35, // 55: memos.api.v1.UserService.UpdateUserNotification:input_type -> memos.api.v1.UpdateUserNotificationRequest
|
||||
36, // 56: memos.api.v1.UserService.DeleteUserNotification:input_type -> memos.api.v1.DeleteUserNotificationRequest
|
||||
6, // 57: memos.api.v1.UserService.ListUsers:output_type -> memos.api.v1.ListUsersResponse
|
||||
4, // 58: memos.api.v1.UserService.GetUser:output_type -> memos.api.v1.User
|
||||
4, // 59: memos.api.v1.UserService.CreateUser:output_type -> memos.api.v1.User
|
||||
4, // 60: memos.api.v1.UserService.UpdateUser:output_type -> memos.api.v1.User
|
||||
45, // 61: memos.api.v1.UserService.DeleteUser:output_type -> google.protobuf.Empty
|
||||
14, // 62: memos.api.v1.UserService.ListAllUserStats:output_type -> memos.api.v1.ListAllUserStatsResponse
|
||||
11, // 63: memos.api.v1.UserService.GetUserStats:output_type -> memos.api.v1.UserStats
|
||||
15, // 64: memos.api.v1.UserService.GetUserSetting:output_type -> memos.api.v1.UserSetting
|
||||
15, // 65: memos.api.v1.UserService.UpdateUserSetting:output_type -> memos.api.v1.UserSetting
|
||||
19, // 66: memos.api.v1.UserService.ListUserSettings:output_type -> memos.api.v1.ListUserSettingsResponse
|
||||
22, // 67: memos.api.v1.UserService.ListPersonalAccessTokens:output_type -> memos.api.v1.ListPersonalAccessTokensResponse
|
||||
24, // 68: memos.api.v1.UserService.CreatePersonalAccessToken:output_type -> memos.api.v1.CreatePersonalAccessTokenResponse
|
||||
45, // 69: memos.api.v1.UserService.DeletePersonalAccessToken:output_type -> google.protobuf.Empty
|
||||
28, // 70: memos.api.v1.UserService.ListUserWebhooks:output_type -> memos.api.v1.ListUserWebhooksResponse
|
||||
26, // 71: memos.api.v1.UserService.CreateUserWebhook:output_type -> memos.api.v1.UserWebhook
|
||||
26, // 72: memos.api.v1.UserService.UpdateUserWebhook:output_type -> memos.api.v1.UserWebhook
|
||||
45, // 73: memos.api.v1.UserService.DeleteUserWebhook:output_type -> google.protobuf.Empty
|
||||
34, // 74: memos.api.v1.UserService.ListUserNotifications:output_type -> memos.api.v1.ListUserNotificationsResponse
|
||||
32, // 75: memos.api.v1.UserService.UpdateUserNotification:output_type -> memos.api.v1.UserNotification
|
||||
45, // 76: memos.api.v1.UserService.DeleteUserNotification:output_type -> google.protobuf.Empty
|
||||
57, // [57:77] is the sub-list for method output_type
|
||||
37, // [37:57] is the sub-list for method input_type
|
||||
37, // [37:37] is the sub-list for extension type_name
|
||||
37, // [37:37] is the sub-list for extension extendee
|
||||
0, // [0:37] is the sub-list for field type_name
|
||||
42, // 15: memos.api.v1.UserSetting.tags_setting:type_name -> memos.api.v1.UserSetting.TagsSetting
|
||||
15, // 16: memos.api.v1.UpdateUserSettingRequest.setting:type_name -> memos.api.v1.UserSetting
|
||||
47, // 17: memos.api.v1.UpdateUserSettingRequest.update_mask:type_name -> google.protobuf.FieldMask
|
||||
15, // 18: memos.api.v1.ListUserSettingsResponse.settings:type_name -> memos.api.v1.UserSetting
|
||||
46, // 19: memos.api.v1.PersonalAccessToken.created_at:type_name -> google.protobuf.Timestamp
|
||||
46, // 20: memos.api.v1.PersonalAccessToken.expires_at:type_name -> google.protobuf.Timestamp
|
||||
46, // 21: memos.api.v1.PersonalAccessToken.last_used_at:type_name -> google.protobuf.Timestamp
|
||||
20, // 22: memos.api.v1.ListPersonalAccessTokensResponse.personal_access_tokens:type_name -> memos.api.v1.PersonalAccessToken
|
||||
20, // 23: memos.api.v1.CreatePersonalAccessTokenResponse.personal_access_token:type_name -> memos.api.v1.PersonalAccessToken
|
||||
46, // 24: memos.api.v1.UserWebhook.create_time:type_name -> google.protobuf.Timestamp
|
||||
46, // 25: memos.api.v1.UserWebhook.update_time:type_name -> google.protobuf.Timestamp
|
||||
26, // 26: memos.api.v1.ListUserWebhooksResponse.webhooks:type_name -> memos.api.v1.UserWebhook
|
||||
26, // 27: memos.api.v1.CreateUserWebhookRequest.webhook:type_name -> memos.api.v1.UserWebhook
|
||||
26, // 28: memos.api.v1.UpdateUserWebhookRequest.webhook:type_name -> memos.api.v1.UserWebhook
|
||||
47, // 29: memos.api.v1.UpdateUserWebhookRequest.update_mask:type_name -> google.protobuf.FieldMask
|
||||
2, // 30: memos.api.v1.UserNotification.status:type_name -> memos.api.v1.UserNotification.Status
|
||||
46, // 31: memos.api.v1.UserNotification.create_time:type_name -> google.protobuf.Timestamp
|
||||
3, // 32: memos.api.v1.UserNotification.type:type_name -> memos.api.v1.UserNotification.Type
|
||||
44, // 33: memos.api.v1.UserNotification.memo_comment:type_name -> memos.api.v1.UserNotification.MemoCommentPayload
|
||||
32, // 34: memos.api.v1.ListUserNotificationsResponse.notifications:type_name -> memos.api.v1.UserNotification
|
||||
32, // 35: memos.api.v1.UpdateUserNotificationRequest.notification:type_name -> memos.api.v1.UserNotification
|
||||
47, // 36: memos.api.v1.UpdateUserNotificationRequest.update_mask:type_name -> google.protobuf.FieldMask
|
||||
26, // 37: memos.api.v1.UserSetting.WebhooksSetting.webhooks:type_name -> memos.api.v1.UserWebhook
|
||||
48, // 38: memos.api.v1.UserSetting.TagMetadata.background_color:type_name -> google.type.Color
|
||||
43, // 39: memos.api.v1.UserSetting.TagsSetting.tags:type_name -> memos.api.v1.UserSetting.TagsSetting.TagsEntry
|
||||
41, // 40: memos.api.v1.UserSetting.TagsSetting.TagsEntry.value:type_name -> memos.api.v1.UserSetting.TagMetadata
|
||||
5, // 41: memos.api.v1.UserService.ListUsers:input_type -> memos.api.v1.ListUsersRequest
|
||||
7, // 42: memos.api.v1.UserService.GetUser:input_type -> memos.api.v1.GetUserRequest
|
||||
8, // 43: memos.api.v1.UserService.CreateUser:input_type -> memos.api.v1.CreateUserRequest
|
||||
9, // 44: memos.api.v1.UserService.UpdateUser:input_type -> memos.api.v1.UpdateUserRequest
|
||||
10, // 45: memos.api.v1.UserService.DeleteUser:input_type -> memos.api.v1.DeleteUserRequest
|
||||
13, // 46: memos.api.v1.UserService.ListAllUserStats:input_type -> memos.api.v1.ListAllUserStatsRequest
|
||||
12, // 47: memos.api.v1.UserService.GetUserStats:input_type -> memos.api.v1.GetUserStatsRequest
|
||||
16, // 48: memos.api.v1.UserService.GetUserSetting:input_type -> memos.api.v1.GetUserSettingRequest
|
||||
17, // 49: memos.api.v1.UserService.UpdateUserSetting:input_type -> memos.api.v1.UpdateUserSettingRequest
|
||||
18, // 50: memos.api.v1.UserService.ListUserSettings:input_type -> memos.api.v1.ListUserSettingsRequest
|
||||
21, // 51: memos.api.v1.UserService.ListPersonalAccessTokens:input_type -> memos.api.v1.ListPersonalAccessTokensRequest
|
||||
23, // 52: memos.api.v1.UserService.CreatePersonalAccessToken:input_type -> memos.api.v1.CreatePersonalAccessTokenRequest
|
||||
25, // 53: memos.api.v1.UserService.DeletePersonalAccessToken:input_type -> memos.api.v1.DeletePersonalAccessTokenRequest
|
||||
27, // 54: memos.api.v1.UserService.ListUserWebhooks:input_type -> memos.api.v1.ListUserWebhooksRequest
|
||||
29, // 55: memos.api.v1.UserService.CreateUserWebhook:input_type -> memos.api.v1.CreateUserWebhookRequest
|
||||
30, // 56: memos.api.v1.UserService.UpdateUserWebhook:input_type -> memos.api.v1.UpdateUserWebhookRequest
|
||||
31, // 57: memos.api.v1.UserService.DeleteUserWebhook:input_type -> memos.api.v1.DeleteUserWebhookRequest
|
||||
33, // 58: memos.api.v1.UserService.ListUserNotifications:input_type -> memos.api.v1.ListUserNotificationsRequest
|
||||
35, // 59: memos.api.v1.UserService.UpdateUserNotification:input_type -> memos.api.v1.UpdateUserNotificationRequest
|
||||
36, // 60: memos.api.v1.UserService.DeleteUserNotification:input_type -> memos.api.v1.DeleteUserNotificationRequest
|
||||
6, // 61: memos.api.v1.UserService.ListUsers:output_type -> memos.api.v1.ListUsersResponse
|
||||
4, // 62: memos.api.v1.UserService.GetUser:output_type -> memos.api.v1.User
|
||||
4, // 63: memos.api.v1.UserService.CreateUser:output_type -> memos.api.v1.User
|
||||
4, // 64: memos.api.v1.UserService.UpdateUser:output_type -> memos.api.v1.User
|
||||
49, // 65: memos.api.v1.UserService.DeleteUser:output_type -> google.protobuf.Empty
|
||||
14, // 66: memos.api.v1.UserService.ListAllUserStats:output_type -> memos.api.v1.ListAllUserStatsResponse
|
||||
11, // 67: memos.api.v1.UserService.GetUserStats:output_type -> memos.api.v1.UserStats
|
||||
15, // 68: memos.api.v1.UserService.GetUserSetting:output_type -> memos.api.v1.UserSetting
|
||||
15, // 69: memos.api.v1.UserService.UpdateUserSetting:output_type -> memos.api.v1.UserSetting
|
||||
19, // 70: memos.api.v1.UserService.ListUserSettings:output_type -> memos.api.v1.ListUserSettingsResponse
|
||||
22, // 71: memos.api.v1.UserService.ListPersonalAccessTokens:output_type -> memos.api.v1.ListPersonalAccessTokensResponse
|
||||
24, // 72: memos.api.v1.UserService.CreatePersonalAccessToken:output_type -> memos.api.v1.CreatePersonalAccessTokenResponse
|
||||
49, // 73: memos.api.v1.UserService.DeletePersonalAccessToken:output_type -> google.protobuf.Empty
|
||||
28, // 74: memos.api.v1.UserService.ListUserWebhooks:output_type -> memos.api.v1.ListUserWebhooksResponse
|
||||
26, // 75: memos.api.v1.UserService.CreateUserWebhook:output_type -> memos.api.v1.UserWebhook
|
||||
26, // 76: memos.api.v1.UserService.UpdateUserWebhook:output_type -> memos.api.v1.UserWebhook
|
||||
49, // 77: memos.api.v1.UserService.DeleteUserWebhook:output_type -> google.protobuf.Empty
|
||||
34, // 78: memos.api.v1.UserService.ListUserNotifications:output_type -> memos.api.v1.ListUserNotificationsResponse
|
||||
32, // 79: memos.api.v1.UserService.UpdateUserNotification:output_type -> memos.api.v1.UserNotification
|
||||
49, // 80: memos.api.v1.UserService.DeleteUserNotification:output_type -> google.protobuf.Empty
|
||||
61, // [61:81] is the sub-list for method output_type
|
||||
41, // [41:61] is the sub-list for method input_type
|
||||
41, // [41:41] is the sub-list for extension type_name
|
||||
41, // [41:41] is the sub-list for extension extendee
|
||||
0, // [0:41] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_api_v1_user_service_proto_init() }
|
||||
|
|
@ -2967,6 +3096,7 @@ func file_api_v1_user_service_proto_init() {
|
|||
file_api_v1_user_service_proto_msgTypes[11].OneofWrappers = []any{
|
||||
(*UserSetting_GeneralSetting_)(nil),
|
||||
(*UserSetting_WebhooksSetting_)(nil),
|
||||
(*UserSetting_TagsSetting_)(nil),
|
||||
}
|
||||
file_api_v1_user_service_proto_msgTypes[28].OneofWrappers = []any{
|
||||
(*UserNotification_MemoComment)(nil),
|
||||
|
|
@ -2977,7 +3107,7 @@ func file_api_v1_user_service_proto_init() {
|
|||
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)),
|
||||
NumEnums: 4,
|
||||
NumMessages: 38,
|
||||
NumMessages: 41,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1903,6 +1903,161 @@ components:
|
|||
description: |-
|
||||
Optional. The related memo. Refer to `Memo.name`.
|
||||
Format: memos/{memo}
|
||||
Color:
|
||||
type: object
|
||||
properties:
|
||||
red:
|
||||
type: number
|
||||
description: The amount of red in the color as a value in the interval [0, 1].
|
||||
format: float
|
||||
green:
|
||||
type: number
|
||||
description: The amount of green in the color as a value in the interval [0, 1].
|
||||
format: float
|
||||
blue:
|
||||
type: number
|
||||
description: The amount of blue in the color as a value in the interval [0, 1].
|
||||
format: float
|
||||
alpha:
|
||||
type: number
|
||||
description: |-
|
||||
The fraction of this color that should be applied to the pixel. That is,
|
||||
the final pixel color is defined by the equation:
|
||||
|
||||
`pixel color = alpha * (this color) + (1.0 - alpha) * (background color)`
|
||||
|
||||
This means that a value of 1.0 corresponds to a solid color, whereas
|
||||
a value of 0.0 corresponds to a completely transparent color. This
|
||||
uses a wrapper message rather than a simple float scalar so that it is
|
||||
possible to distinguish between a default value and the value being unset.
|
||||
If omitted, this color object is rendered as a solid color
|
||||
(as if the alpha value had been explicitly given a value of 1.0).
|
||||
format: float
|
||||
description: |-
|
||||
Represents a color in the RGBA color space. This representation is designed
|
||||
for simplicity of conversion to/from color representations in various
|
||||
languages over compactness. For example, the fields of this representation
|
||||
can be trivially provided to the constructor of `java.awt.Color` in Java; it
|
||||
can also be trivially provided to UIColor's `+colorWithRed:green:blue:alpha`
|
||||
method in iOS; and, with just a little work, it can be easily formatted into
|
||||
a CSS `rgba()` string in JavaScript.
|
||||
|
||||
This reference page doesn't carry information about the absolute color
|
||||
space
|
||||
that should be used to interpret the RGB value (e.g. sRGB, Adobe RGB,
|
||||
DCI-P3, BT.2020, etc.). By default, applications should assume the sRGB color
|
||||
space.
|
||||
|
||||
When color equality needs to be decided, implementations, unless
|
||||
documented otherwise, treat two colors as equal if all their red,
|
||||
green, blue, and alpha values each differ by at most 1e-5.
|
||||
|
||||
Example (Java):
|
||||
|
||||
import com.google.type.Color;
|
||||
|
||||
// ...
|
||||
public static java.awt.Color fromProto(Color protocolor) {
|
||||
float alpha = protocolor.hasAlpha()
|
||||
? protocolor.getAlpha().getValue()
|
||||
: 1.0;
|
||||
|
||||
return new java.awt.Color(
|
||||
protocolor.getRed(),
|
||||
protocolor.getGreen(),
|
||||
protocolor.getBlue(),
|
||||
alpha);
|
||||
}
|
||||
|
||||
public static Color toProto(java.awt.Color color) {
|
||||
float red = (float) color.getRed();
|
||||
float green = (float) color.getGreen();
|
||||
float blue = (float) color.getBlue();
|
||||
float denominator = 255.0;
|
||||
Color.Builder resultBuilder =
|
||||
Color
|
||||
.newBuilder()
|
||||
.setRed(red / denominator)
|
||||
.setGreen(green / denominator)
|
||||
.setBlue(blue / denominator);
|
||||
int alpha = color.getAlpha();
|
||||
if (alpha != 255) {
|
||||
result.setAlpha(
|
||||
FloatValue
|
||||
.newBuilder()
|
||||
.setValue(((float) alpha) / denominator)
|
||||
.build());
|
||||
}
|
||||
return resultBuilder.build();
|
||||
}
|
||||
// ...
|
||||
|
||||
Example (iOS / Obj-C):
|
||||
|
||||
// ...
|
||||
static UIColor* fromProto(Color* protocolor) {
|
||||
float red = [protocolor red];
|
||||
float green = [protocolor green];
|
||||
float blue = [protocolor blue];
|
||||
FloatValue* alpha_wrapper = [protocolor alpha];
|
||||
float alpha = 1.0;
|
||||
if (alpha_wrapper != nil) {
|
||||
alpha = [alpha_wrapper value];
|
||||
}
|
||||
return [UIColor colorWithRed:red green:green blue:blue alpha:alpha];
|
||||
}
|
||||
|
||||
static Color* toProto(UIColor* color) {
|
||||
CGFloat red, green, blue, alpha;
|
||||
if (![color getRed:&red green:&green blue:&blue alpha:&alpha]) {
|
||||
return nil;
|
||||
}
|
||||
Color* result = [[Color alloc] init];
|
||||
[result setRed:red];
|
||||
[result setGreen:green];
|
||||
[result setBlue:blue];
|
||||
if (alpha <= 0.9999) {
|
||||
[result setAlpha:floatWrapperWithValue(alpha)];
|
||||
}
|
||||
[result autorelease];
|
||||
return result;
|
||||
}
|
||||
// ...
|
||||
|
||||
Example (JavaScript):
|
||||
|
||||
// ...
|
||||
|
||||
var protoToCssColor = function(rgb_color) {
|
||||
var redFrac = rgb_color.red || 0.0;
|
||||
var greenFrac = rgb_color.green || 0.0;
|
||||
var blueFrac = rgb_color.blue || 0.0;
|
||||
var red = Math.floor(redFrac * 255);
|
||||
var green = Math.floor(greenFrac * 255);
|
||||
var blue = Math.floor(blueFrac * 255);
|
||||
|
||||
if (!('alpha' in rgb_color)) {
|
||||
return rgbToCssColor(red, green, blue);
|
||||
}
|
||||
|
||||
var alphaFrac = rgb_color.alpha.value || 0.0;
|
||||
var rgbParams = [red, green, blue].join(',');
|
||||
return ['rgba(', rgbParams, ',', alphaFrac, ')'].join('');
|
||||
};
|
||||
|
||||
var rgbToCssColor = function(red, green, blue) {
|
||||
var rgbNumber = new Number((red << 16) | (green << 8) | blue);
|
||||
var hexString = rgbNumber.toString(16);
|
||||
var missingZeros = 6 - hexString.length;
|
||||
var resultBuilder = ['#'];
|
||||
for (var i = 0; i < missingZeros; i++) {
|
||||
resultBuilder.push('0');
|
||||
}
|
||||
resultBuilder.push(hexString);
|
||||
return resultBuilder.join('');
|
||||
};
|
||||
|
||||
// ...
|
||||
CreatePersonalAccessTokenRequest:
|
||||
required:
|
||||
- parent
|
||||
|
|
@ -2831,6 +2986,8 @@ components:
|
|||
$ref: '#/components/schemas/UserSetting_GeneralSetting'
|
||||
webhooksSetting:
|
||||
$ref: '#/components/schemas/UserSetting_WebhooksSetting'
|
||||
tagsSetting:
|
||||
$ref: '#/components/schemas/UserSetting_TagsSetting'
|
||||
description: User settings message
|
||||
UserSetting_GeneralSetting:
|
||||
type: object
|
||||
|
|
@ -2848,6 +3005,22 @@ components:
|
|||
This references a CSS file in the web/public/themes/ directory.
|
||||
If not set, the default theme will be used.
|
||||
description: General user settings configuration.
|
||||
UserSetting_TagMetadata:
|
||||
type: object
|
||||
properties:
|
||||
backgroundColor:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Color'
|
||||
description: Background color for the tag label.
|
||||
description: Metadata for a tag.
|
||||
UserSetting_TagsSetting:
|
||||
type: object
|
||||
properties:
|
||||
tags:
|
||||
type: object
|
||||
additionalProperties:
|
||||
$ref: '#/components/schemas/UserSetting_TagMetadata'
|
||||
description: User tag metadata configuration.
|
||||
UserSetting_WebhooksSetting:
|
||||
type: object
|
||||
properties:
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
package store
|
||||
|
||||
import (
|
||||
color "google.golang.org/genproto/googleapis/type/color"
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
|
@ -36,6 +37,8 @@ const (
|
|||
UserSetting_REFRESH_TOKENS UserSetting_Key = 6
|
||||
// Personal access tokens for the user.
|
||||
UserSetting_PERSONAL_ACCESS_TOKENS UserSetting_Key = 7
|
||||
// Tag metadata for the user.
|
||||
UserSetting_TAGS UserSetting_Key = 8
|
||||
)
|
||||
|
||||
// Enum value maps for UserSetting_Key.
|
||||
|
|
@ -47,6 +50,7 @@ var (
|
|||
5: "WEBHOOKS",
|
||||
6: "REFRESH_TOKENS",
|
||||
7: "PERSONAL_ACCESS_TOKENS",
|
||||
8: "TAGS",
|
||||
}
|
||||
UserSetting_Key_value = map[string]int32{
|
||||
"KEY_UNSPECIFIED": 0,
|
||||
|
|
@ -55,6 +59,7 @@ var (
|
|||
"WEBHOOKS": 5,
|
||||
"REFRESH_TOKENS": 6,
|
||||
"PERSONAL_ACCESS_TOKENS": 7,
|
||||
"TAGS": 8,
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -96,6 +101,7 @@ type UserSetting struct {
|
|||
// *UserSetting_Webhooks
|
||||
// *UserSetting_RefreshTokens
|
||||
// *UserSetting_PersonalAccessTokens
|
||||
// *UserSetting_Tags
|
||||
Value isUserSetting_Value `protobuf_oneof:"value"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
|
|
@ -197,6 +203,15 @@ func (x *UserSetting) GetPersonalAccessTokens() *PersonalAccessTokensUserSetting
|
|||
return nil
|
||||
}
|
||||
|
||||
func (x *UserSetting) GetTags() *TagsUserSetting {
|
||||
if x != nil {
|
||||
if x, ok := x.Value.(*UserSetting_Tags); ok {
|
||||
return x.Tags
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type isUserSetting_Value interface {
|
||||
isUserSetting_Value()
|
||||
}
|
||||
|
|
@ -221,6 +236,10 @@ type UserSetting_PersonalAccessTokens struct {
|
|||
PersonalAccessTokens *PersonalAccessTokensUserSetting `protobuf:"bytes,9,opt,name=personal_access_tokens,json=personalAccessTokens,proto3,oneof"`
|
||||
}
|
||||
|
||||
type UserSetting_Tags struct {
|
||||
Tags *TagsUserSetting `protobuf:"bytes,10,opt,name=tags,proto3,oneof"`
|
||||
}
|
||||
|
||||
func (*UserSetting_General) isUserSetting_Value() {}
|
||||
|
||||
func (*UserSetting_Shortcuts) isUserSetting_Value() {}
|
||||
|
|
@ -231,6 +250,8 @@ func (*UserSetting_RefreshTokens) isUserSetting_Value() {}
|
|||
|
||||
func (*UserSetting_PersonalAccessTokens) isUserSetting_Value() {}
|
||||
|
||||
func (*UserSetting_Tags) isUserSetting_Value() {}
|
||||
|
||||
type GeneralUserSetting struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// The user's locale.
|
||||
|
|
@ -471,6 +492,95 @@ func (x *WebhooksUserSetting) GetWebhooks() []*WebhooksUserSetting_Webhook {
|
|||
return nil
|
||||
}
|
||||
|
||||
type TagMetadata struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// Background color for the tag label.
|
||||
BackgroundColor *color.Color `protobuf:"bytes,1,opt,name=background_color,json=backgroundColor,proto3" json:"background_color,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *TagMetadata) Reset() {
|
||||
*x = TagMetadata{}
|
||||
mi := &file_store_user_setting_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *TagMetadata) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*TagMetadata) ProtoMessage() {}
|
||||
|
||||
func (x *TagMetadata) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_store_user_setting_proto_msgTypes[6]
|
||||
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 TagMetadata.ProtoReflect.Descriptor instead.
|
||||
func (*TagMetadata) Descriptor() ([]byte, []int) {
|
||||
return file_store_user_setting_proto_rawDescGZIP(), []int{6}
|
||||
}
|
||||
|
||||
func (x *TagMetadata) GetBackgroundColor() *color.Color {
|
||||
if x != nil {
|
||||
return x.BackgroundColor
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type TagsUserSetting struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Tags map[string]*TagMetadata `protobuf:"bytes,1,rep,name=tags,proto3" json:"tags,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *TagsUserSetting) Reset() {
|
||||
*x = TagsUserSetting{}
|
||||
mi := &file_store_user_setting_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *TagsUserSetting) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*TagsUserSetting) ProtoMessage() {}
|
||||
|
||||
func (x *TagsUserSetting) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_store_user_setting_proto_msgTypes[7]
|
||||
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 TagsUserSetting.ProtoReflect.Descriptor instead.
|
||||
func (*TagsUserSetting) Descriptor() ([]byte, []int) {
|
||||
return file_store_user_setting_proto_rawDescGZIP(), []int{7}
|
||||
}
|
||||
|
||||
func (x *TagsUserSetting) GetTags() map[string]*TagMetadata {
|
||||
if x != nil {
|
||||
return x.Tags
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type RefreshTokensUserSetting_RefreshToken struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// Unique identifier (matches 'tid' claim in JWT)
|
||||
|
|
@ -489,7 +599,7 @@ type RefreshTokensUserSetting_RefreshToken struct {
|
|||
|
||||
func (x *RefreshTokensUserSetting_RefreshToken) Reset() {
|
||||
*x = RefreshTokensUserSetting_RefreshToken{}
|
||||
mi := &file_store_user_setting_proto_msgTypes[6]
|
||||
mi := &file_store_user_setting_proto_msgTypes[8]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
|
@ -501,7 +611,7 @@ func (x *RefreshTokensUserSetting_RefreshToken) String() string {
|
|||
func (*RefreshTokensUserSetting_RefreshToken) ProtoMessage() {}
|
||||
|
||||
func (x *RefreshTokensUserSetting_RefreshToken) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_store_user_setting_proto_msgTypes[6]
|
||||
mi := &file_store_user_setting_proto_msgTypes[8]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
|
|
@ -570,7 +680,7 @@ type RefreshTokensUserSetting_ClientInfo struct {
|
|||
|
||||
func (x *RefreshTokensUserSetting_ClientInfo) Reset() {
|
||||
*x = RefreshTokensUserSetting_ClientInfo{}
|
||||
mi := &file_store_user_setting_proto_msgTypes[7]
|
||||
mi := &file_store_user_setting_proto_msgTypes[9]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
|
@ -582,7 +692,7 @@ func (x *RefreshTokensUserSetting_ClientInfo) String() string {
|
|||
func (*RefreshTokensUserSetting_ClientInfo) ProtoMessage() {}
|
||||
|
||||
func (x *RefreshTokensUserSetting_ClientInfo) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_store_user_setting_proto_msgTypes[7]
|
||||
mi := &file_store_user_setting_proto_msgTypes[9]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
|
|
@ -653,7 +763,7 @@ type PersonalAccessTokensUserSetting_PersonalAccessToken struct {
|
|||
|
||||
func (x *PersonalAccessTokensUserSetting_PersonalAccessToken) Reset() {
|
||||
*x = PersonalAccessTokensUserSetting_PersonalAccessToken{}
|
||||
mi := &file_store_user_setting_proto_msgTypes[8]
|
||||
mi := &file_store_user_setting_proto_msgTypes[10]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
|
@ -665,7 +775,7 @@ func (x *PersonalAccessTokensUserSetting_PersonalAccessToken) String() string {
|
|||
func (*PersonalAccessTokensUserSetting_PersonalAccessToken) ProtoMessage() {}
|
||||
|
||||
func (x *PersonalAccessTokensUserSetting_PersonalAccessToken) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_store_user_setting_proto_msgTypes[8]
|
||||
mi := &file_store_user_setting_proto_msgTypes[10]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
|
|
@ -734,7 +844,7 @@ type ShortcutsUserSetting_Shortcut struct {
|
|||
|
||||
func (x *ShortcutsUserSetting_Shortcut) Reset() {
|
||||
*x = ShortcutsUserSetting_Shortcut{}
|
||||
mi := &file_store_user_setting_proto_msgTypes[9]
|
||||
mi := &file_store_user_setting_proto_msgTypes[11]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
|
@ -746,7 +856,7 @@ func (x *ShortcutsUserSetting_Shortcut) String() string {
|
|||
func (*ShortcutsUserSetting_Shortcut) ProtoMessage() {}
|
||||
|
||||
func (x *ShortcutsUserSetting_Shortcut) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_store_user_setting_proto_msgTypes[9]
|
||||
mi := &file_store_user_setting_proto_msgTypes[11]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
|
|
@ -797,7 +907,7 @@ type WebhooksUserSetting_Webhook struct {
|
|||
|
||||
func (x *WebhooksUserSetting_Webhook) Reset() {
|
||||
*x = WebhooksUserSetting_Webhook{}
|
||||
mi := &file_store_user_setting_proto_msgTypes[10]
|
||||
mi := &file_store_user_setting_proto_msgTypes[12]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
|
@ -809,7 +919,7 @@ func (x *WebhooksUserSetting_Webhook) String() string {
|
|||
func (*WebhooksUserSetting_Webhook) ProtoMessage() {}
|
||||
|
||||
func (x *WebhooksUserSetting_Webhook) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_store_user_setting_proto_msgTypes[10]
|
||||
mi := &file_store_user_setting_proto_msgTypes[12]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
|
|
@ -850,7 +960,7 @@ var File_store_user_setting_proto protoreflect.FileDescriptor
|
|||
|
||||
const file_store_user_setting_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x18store/user_setting.proto\x12\vmemos.store\x1a\x1fgoogle/protobuf/timestamp.proto\"\xcb\x04\n" +
|
||||
"\x18store/user_setting.proto\x12\vmemos.store\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x17google/type/color.proto\"\x89\x05\n" +
|
||||
"\vUserSetting\x12\x17\n" +
|
||||
"\auser_id\x18\x01 \x01(\x05R\x06userId\x12.\n" +
|
||||
"\x03key\x18\x02 \x01(\x0e2\x1c.memos.store.UserSetting.KeyR\x03key\x12;\n" +
|
||||
|
|
@ -858,14 +968,17 @@ const file_store_user_setting_proto_rawDesc = "" +
|
|||
"\tshortcuts\x18\x06 \x01(\v2!.memos.store.ShortcutsUserSettingH\x00R\tshortcuts\x12>\n" +
|
||||
"\bwebhooks\x18\a \x01(\v2 .memos.store.WebhooksUserSettingH\x00R\bwebhooks\x12N\n" +
|
||||
"\x0erefresh_tokens\x18\b \x01(\v2%.memos.store.RefreshTokensUserSettingH\x00R\rrefreshTokens\x12d\n" +
|
||||
"\x16personal_access_tokens\x18\t \x01(\v2,.memos.store.PersonalAccessTokensUserSettingH\x00R\x14personalAccessTokens\"t\n" +
|
||||
"\x16personal_access_tokens\x18\t \x01(\v2,.memos.store.PersonalAccessTokensUserSettingH\x00R\x14personalAccessTokens\x122\n" +
|
||||
"\x04tags\x18\n" +
|
||||
" \x01(\v2\x1c.memos.store.TagsUserSettingH\x00R\x04tags\"~\n" +
|
||||
"\x03Key\x12\x13\n" +
|
||||
"\x0fKEY_UNSPECIFIED\x10\x00\x12\v\n" +
|
||||
"\aGENERAL\x10\x01\x12\r\n" +
|
||||
"\tSHORTCUTS\x10\x04\x12\f\n" +
|
||||
"\bWEBHOOKS\x10\x05\x12\x12\n" +
|
||||
"\x0eREFRESH_TOKENS\x10\x06\x12\x1a\n" +
|
||||
"\x16PERSONAL_ACCESS_TOKENS\x10\aB\a\n" +
|
||||
"\x16PERSONAL_ACCESS_TOKENS\x10\a\x12\b\n" +
|
||||
"\x04TAGS\x10\bB\a\n" +
|
||||
"\x05value\"k\n" +
|
||||
"\x12GeneralUserSetting\x12\x16\n" +
|
||||
"\x06locale\x18\x01 \x01(\tR\x06locale\x12'\n" +
|
||||
|
|
@ -916,7 +1029,14 @@ const file_store_user_setting_proto_rawDesc = "" +
|
|||
"\aWebhook\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\tR\x02id\x12\x14\n" +
|
||||
"\x05title\x18\x02 \x01(\tR\x05title\x12\x10\n" +
|
||||
"\x03url\x18\x03 \x01(\tR\x03urlB\x9b\x01\n" +
|
||||
"\x03url\x18\x03 \x01(\tR\x03url\"L\n" +
|
||||
"\vTagMetadata\x12=\n" +
|
||||
"\x10background_color\x18\x01 \x01(\v2\x12.google.type.ColorR\x0fbackgroundColor\"\xa0\x01\n" +
|
||||
"\x0fTagsUserSetting\x12:\n" +
|
||||
"\x04tags\x18\x01 \x03(\v2&.memos.store.TagsUserSetting.TagsEntryR\x04tags\x1aQ\n" +
|
||||
"\tTagsEntry\x12\x10\n" +
|
||||
"\x03key\x18\x01 \x01(\tR\x03key\x12.\n" +
|
||||
"\x05value\x18\x02 \x01(\v2\x18.memos.store.TagMetadataR\x05value:\x028\x01B\x9b\x01\n" +
|
||||
"\x0fcom.memos.storeB\x10UserSettingProtoP\x01Z)github.com/usememos/memos/proto/gen/store\xa2\x02\x03MSX\xaa\x02\vMemos.Store\xca\x02\vMemos\\Store\xe2\x02\x17Memos\\Store\\GPBMetadata\xea\x02\fMemos::Storeb\x06proto3"
|
||||
|
||||
var (
|
||||
|
|
@ -932,7 +1052,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_msgTypes = make([]protoimpl.MessageInfo, 11)
|
||||
var file_store_user_setting_proto_msgTypes = make([]protoimpl.MessageInfo, 14)
|
||||
var file_store_user_setting_proto_goTypes = []any{
|
||||
(UserSetting_Key)(0), // 0: memos.store.UserSetting.Key
|
||||
(*UserSetting)(nil), // 1: memos.store.UserSetting
|
||||
|
|
@ -941,12 +1061,16 @@ var file_store_user_setting_proto_goTypes = []any{
|
|||
(*PersonalAccessTokensUserSetting)(nil), // 4: memos.store.PersonalAccessTokensUserSetting
|
||||
(*ShortcutsUserSetting)(nil), // 5: memos.store.ShortcutsUserSetting
|
||||
(*WebhooksUserSetting)(nil), // 6: memos.store.WebhooksUserSetting
|
||||
(*RefreshTokensUserSetting_RefreshToken)(nil), // 7: memos.store.RefreshTokensUserSetting.RefreshToken
|
||||
(*RefreshTokensUserSetting_ClientInfo)(nil), // 8: memos.store.RefreshTokensUserSetting.ClientInfo
|
||||
(*PersonalAccessTokensUserSetting_PersonalAccessToken)(nil), // 9: memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken
|
||||
(*ShortcutsUserSetting_Shortcut)(nil), // 10: memos.store.ShortcutsUserSetting.Shortcut
|
||||
(*WebhooksUserSetting_Webhook)(nil), // 11: memos.store.WebhooksUserSetting.Webhook
|
||||
(*timestamppb.Timestamp)(nil), // 12: google.protobuf.Timestamp
|
||||
(*TagMetadata)(nil), // 7: memos.store.TagMetadata
|
||||
(*TagsUserSetting)(nil), // 8: memos.store.TagsUserSetting
|
||||
(*RefreshTokensUserSetting_RefreshToken)(nil), // 9: memos.store.RefreshTokensUserSetting.RefreshToken
|
||||
(*RefreshTokensUserSetting_ClientInfo)(nil), // 10: memos.store.RefreshTokensUserSetting.ClientInfo
|
||||
(*PersonalAccessTokensUserSetting_PersonalAccessToken)(nil), // 11: memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken
|
||||
(*ShortcutsUserSetting_Shortcut)(nil), // 12: memos.store.ShortcutsUserSetting.Shortcut
|
||||
(*WebhooksUserSetting_Webhook)(nil), // 13: memos.store.WebhooksUserSetting.Webhook
|
||||
nil, // 14: memos.store.TagsUserSetting.TagsEntry
|
||||
(*color.Color)(nil), // 15: google.type.Color
|
||||
(*timestamppb.Timestamp)(nil), // 16: google.protobuf.Timestamp
|
||||
}
|
||||
var file_store_user_setting_proto_depIdxs = []int32{
|
||||
0, // 0: memos.store.UserSetting.key:type_name -> memos.store.UserSetting.Key
|
||||
|
|
@ -955,21 +1079,25 @@ var file_store_user_setting_proto_depIdxs = []int32{
|
|||
6, // 3: memos.store.UserSetting.webhooks:type_name -> memos.store.WebhooksUserSetting
|
||||
3, // 4: memos.store.UserSetting.refresh_tokens:type_name -> memos.store.RefreshTokensUserSetting
|
||||
4, // 5: memos.store.UserSetting.personal_access_tokens:type_name -> memos.store.PersonalAccessTokensUserSetting
|
||||
7, // 6: memos.store.RefreshTokensUserSetting.refresh_tokens:type_name -> memos.store.RefreshTokensUserSetting.RefreshToken
|
||||
9, // 7: memos.store.PersonalAccessTokensUserSetting.tokens:type_name -> memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken
|
||||
10, // 8: memos.store.ShortcutsUserSetting.shortcuts:type_name -> memos.store.ShortcutsUserSetting.Shortcut
|
||||
11, // 9: memos.store.WebhooksUserSetting.webhooks:type_name -> memos.store.WebhooksUserSetting.Webhook
|
||||
12, // 10: memos.store.RefreshTokensUserSetting.RefreshToken.expires_at:type_name -> google.protobuf.Timestamp
|
||||
12, // 11: memos.store.RefreshTokensUserSetting.RefreshToken.created_at:type_name -> google.protobuf.Timestamp
|
||||
8, // 12: memos.store.RefreshTokensUserSetting.RefreshToken.client_info:type_name -> memos.store.RefreshTokensUserSetting.ClientInfo
|
||||
12, // 13: memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken.expires_at:type_name -> google.protobuf.Timestamp
|
||||
12, // 14: memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken.created_at:type_name -> google.protobuf.Timestamp
|
||||
12, // 15: memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken.last_used_at:type_name -> google.protobuf.Timestamp
|
||||
16, // [16:16] is the sub-list for method output_type
|
||||
16, // [16:16] is the sub-list for method input_type
|
||||
16, // [16:16] is the sub-list for extension type_name
|
||||
16, // [16:16] is the sub-list for extension extendee
|
||||
0, // [0:16] is the sub-list for field type_name
|
||||
8, // 6: memos.store.UserSetting.tags:type_name -> memos.store.TagsUserSetting
|
||||
9, // 7: memos.store.RefreshTokensUserSetting.refresh_tokens:type_name -> memos.store.RefreshTokensUserSetting.RefreshToken
|
||||
11, // 8: memos.store.PersonalAccessTokensUserSetting.tokens:type_name -> memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken
|
||||
12, // 9: memos.store.ShortcutsUserSetting.shortcuts:type_name -> memos.store.ShortcutsUserSetting.Shortcut
|
||||
13, // 10: memos.store.WebhooksUserSetting.webhooks:type_name -> memos.store.WebhooksUserSetting.Webhook
|
||||
15, // 11: memos.store.TagMetadata.background_color:type_name -> google.type.Color
|
||||
14, // 12: memos.store.TagsUserSetting.tags:type_name -> memos.store.TagsUserSetting.TagsEntry
|
||||
16, // 13: memos.store.RefreshTokensUserSetting.RefreshToken.expires_at:type_name -> google.protobuf.Timestamp
|
||||
16, // 14: memos.store.RefreshTokensUserSetting.RefreshToken.created_at:type_name -> google.protobuf.Timestamp
|
||||
10, // 15: memos.store.RefreshTokensUserSetting.RefreshToken.client_info:type_name -> memos.store.RefreshTokensUserSetting.ClientInfo
|
||||
16, // 16: memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken.expires_at:type_name -> google.protobuf.Timestamp
|
||||
16, // 17: memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken.created_at:type_name -> google.protobuf.Timestamp
|
||||
16, // 18: memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken.last_used_at:type_name -> google.protobuf.Timestamp
|
||||
7, // 19: memos.store.TagsUserSetting.TagsEntry.value:type_name -> memos.store.TagMetadata
|
||||
20, // [20:20] is the sub-list for method output_type
|
||||
20, // [20:20] is the sub-list for method input_type
|
||||
20, // [20:20] is the sub-list for extension type_name
|
||||
20, // [20:20] is the sub-list for extension extendee
|
||||
0, // [0:20] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_store_user_setting_proto_init() }
|
||||
|
|
@ -983,6 +1111,7 @@ func file_store_user_setting_proto_init() {
|
|||
(*UserSetting_Webhooks)(nil),
|
||||
(*UserSetting_RefreshTokens)(nil),
|
||||
(*UserSetting_PersonalAccessTokens)(nil),
|
||||
(*UserSetting_Tags)(nil),
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
|
|
@ -990,7 +1119,7 @@ func file_store_user_setting_proto_init() {
|
|||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_store_user_setting_proto_rawDesc), len(file_store_user_setting_proto_rawDesc)),
|
||||
NumEnums: 1,
|
||||
NumMessages: 11,
|
||||
NumMessages: 14,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ syntax = "proto3";
|
|||
package memos.store;
|
||||
|
||||
import "google/protobuf/timestamp.proto";
|
||||
import "google/type/color.proto";
|
||||
|
||||
option go_package = "gen/store";
|
||||
|
||||
|
|
@ -19,6 +20,8 @@ message UserSetting {
|
|||
REFRESH_TOKENS = 6;
|
||||
// Personal access tokens for the user.
|
||||
PERSONAL_ACCESS_TOKENS = 7;
|
||||
// Tag metadata for the user.
|
||||
TAGS = 8;
|
||||
}
|
||||
|
||||
int32 user_id = 1;
|
||||
|
|
@ -30,6 +33,7 @@ message UserSetting {
|
|||
WebhooksUserSetting webhooks = 7;
|
||||
RefreshTokensUserSetting refresh_tokens = 8;
|
||||
PersonalAccessTokensUserSetting personal_access_tokens = 9;
|
||||
TagsUserSetting tags = 10;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -111,3 +115,12 @@ message WebhooksUserSetting {
|
|||
}
|
||||
repeated Webhook webhooks = 1;
|
||||
}
|
||||
|
||||
message TagMetadata {
|
||||
// Background color for the tag label.
|
||||
google.type.Color background_color = 1;
|
||||
}
|
||||
|
||||
message TagsUserSetting {
|
||||
map<string, TagMetadata> tags = 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,144 @@
|
|||
package test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
colorpb "google.golang.org/genproto/googleapis/type/color"
|
||||
"google.golang.org/protobuf/types/known/fieldmaskpb"
|
||||
|
||||
apiv1 "github.com/usememos/memos/proto/gen/api/v1"
|
||||
)
|
||||
|
||||
func TestUserSettingTags(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("GetUserSetting returns empty tags setting by default", func(t *testing.T) {
|
||||
ts := NewTestService(t)
|
||||
defer ts.Cleanup()
|
||||
|
||||
user, err := ts.CreateHostUser(ctx, "tags-default")
|
||||
require.NoError(t, err)
|
||||
|
||||
response, err := ts.Service.GetUserSetting(ts.CreateUserContext(ctx, user.ID), &apiv1.GetUserSettingRequest{
|
||||
Name: fmt.Sprintf("users/%d/settings/TAGS", user.ID),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, response)
|
||||
require.NotNil(t, response.GetTagsSetting())
|
||||
require.Empty(t, response.GetTagsSetting().GetTags())
|
||||
})
|
||||
|
||||
t.Run("UpdateUserSetting replaces tag metadata", func(t *testing.T) {
|
||||
ts := NewTestService(t)
|
||||
defer ts.Cleanup()
|
||||
|
||||
user, err := ts.CreateHostUser(ctx, "tags-update")
|
||||
require.NoError(t, err)
|
||||
userCtx := ts.CreateUserContext(ctx, user.ID)
|
||||
|
||||
settingName := fmt.Sprintf("users/%d/settings/TAGS", user.ID)
|
||||
updateRequest := &apiv1.UpdateUserSettingRequest{
|
||||
Setting: &apiv1.UserSetting{
|
||||
Name: settingName,
|
||||
Value: &apiv1.UserSetting_TagsSetting_{
|
||||
TagsSetting: &apiv1.UserSetting_TagsSetting{
|
||||
Tags: map[string]*apiv1.UserSetting_TagMetadata{
|
||||
"bug": {
|
||||
BackgroundColor: &colorpb.Color{
|
||||
Red: 0.9,
|
||||
Green: 0.1,
|
||||
Blue: 0.1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"tags"}},
|
||||
}
|
||||
|
||||
response, err := ts.Service.UpdateUserSetting(userCtx, updateRequest)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, response.GetTagsSetting())
|
||||
require.Contains(t, response.GetTagsSetting().GetTags(), "bug")
|
||||
require.InDelta(t, 0.9, response.GetTagsSetting().GetTags()["bug"].GetBackgroundColor().GetRed(), 0.0001)
|
||||
|
||||
getResponse, err := ts.Service.GetUserSetting(userCtx, &apiv1.GetUserSettingRequest{Name: settingName})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, getResponse.GetTagsSetting().GetTags(), 1)
|
||||
require.Contains(t, getResponse.GetTagsSetting().GetTags(), "bug")
|
||||
})
|
||||
|
||||
t.Run("UpdateUserSetting rejects invalid color", func(t *testing.T) {
|
||||
ts := NewTestService(t)
|
||||
defer ts.Cleanup()
|
||||
|
||||
user, err := ts.CreateHostUser(ctx, "tags-invalid")
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = ts.Service.UpdateUserSetting(ts.CreateUserContext(ctx, user.ID), &apiv1.UpdateUserSettingRequest{
|
||||
Setting: &apiv1.UserSetting{
|
||||
Name: fmt.Sprintf("users/%d/settings/TAGS", user.ID),
|
||||
Value: &apiv1.UserSetting_TagsSetting_{
|
||||
TagsSetting: &apiv1.UserSetting_TagsSetting{
|
||||
Tags: map[string]*apiv1.UserSetting_TagMetadata{
|
||||
"bug": {
|
||||
BackgroundColor: &colorpb.Color{
|
||||
Red: 1.2,
|
||||
Green: 0.1,
|
||||
Blue: 0.1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"tags"}},
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "invalid tags setting")
|
||||
})
|
||||
|
||||
t.Run("Other users cannot read or update tag metadata", func(t *testing.T) {
|
||||
ts := NewTestService(t)
|
||||
defer ts.Cleanup()
|
||||
|
||||
user, err := ts.CreateHostUser(ctx, "tags-owner")
|
||||
require.NoError(t, err)
|
||||
otherUser, err := ts.CreateHostUser(ctx, "tags-other")
|
||||
require.NoError(t, err)
|
||||
|
||||
settingName := fmt.Sprintf("users/%d/settings/TAGS", user.ID)
|
||||
_, err = ts.Service.GetUserSetting(ts.CreateUserContext(ctx, otherUser.ID), &apiv1.GetUserSettingRequest{
|
||||
Name: settingName,
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "permission denied")
|
||||
|
||||
_, err = ts.Service.UpdateUserSetting(ts.CreateUserContext(ctx, otherUser.ID), &apiv1.UpdateUserSettingRequest{
|
||||
Setting: &apiv1.UserSetting{
|
||||
Name: settingName,
|
||||
Value: &apiv1.UserSetting_TagsSetting_{
|
||||
TagsSetting: &apiv1.UserSetting_TagsSetting{
|
||||
Tags: map[string]*apiv1.UserSetting_TagMetadata{
|
||||
"bug": {
|
||||
BackgroundColor: &colorpb.Color{
|
||||
Red: 0.1,
|
||||
Green: 0.2,
|
||||
Blue: 0.3,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"tags"}},
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "permission denied")
|
||||
})
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
|
@ -14,6 +15,7 @@ import (
|
|||
"github.com/google/cel-go/common/ast"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
colorpb "google.golang.org/genproto/googleapis/type/color"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
|
|
@ -332,6 +334,12 @@ func getDefaultUserGeneralSetting() *v1pb.UserSetting_GeneralSetting {
|
|||
}
|
||||
}
|
||||
|
||||
func getDefaultUserTagsSetting() *v1pb.UserSetting_TagsSetting {
|
||||
return &v1pb.UserSetting_TagsSetting{
|
||||
Tags: map[string]*v1pb.UserSetting_TagMetadata{},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *APIV1Service) GetUserSetting(ctx context.Context, request *v1pb.GetUserSettingRequest) (*v1pb.UserSetting, error) {
|
||||
// Parse resource name: users/{user}/settings/{setting}
|
||||
userID, settingKey, err := ExtractUserIDAndSettingKeyFromName(request.Name)
|
||||
|
|
@ -399,50 +407,69 @@ func (s *APIV1Service) UpdateUserSetting(ctx context.Context, request *v1pb.Upda
|
|||
return nil, status.Errorf(codes.InvalidArgument, "invalid setting key: %v", err)
|
||||
}
|
||||
|
||||
// Only GENERAL settings are supported via UpdateUserSetting
|
||||
// Other setting types have dedicated service methods
|
||||
if storeKey != storepb.UserSetting_GENERAL {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "setting type %s should not be updated via UpdateUserSetting", storeKey.String())
|
||||
}
|
||||
var updatedSetting *v1pb.UserSetting
|
||||
switch storeKey {
|
||||
case storepb.UserSetting_GENERAL:
|
||||
existingUserSetting, _ := s.Store.GetUserSetting(ctx, &store.FindUserSetting{
|
||||
UserID: &userID,
|
||||
Key: storeKey,
|
||||
})
|
||||
|
||||
existingUserSetting, _ := s.Store.GetUserSetting(ctx, &store.FindUserSetting{
|
||||
UserID: &userID,
|
||||
Key: storeKey,
|
||||
})
|
||||
|
||||
generalSetting := &storepb.GeneralUserSetting{}
|
||||
if existingUserSetting != nil {
|
||||
// Start with existing general setting values
|
||||
generalSetting = existingUserSetting.GetGeneral()
|
||||
}
|
||||
|
||||
updatedGeneral := &v1pb.UserSetting_GeneralSetting{
|
||||
MemoVisibility: generalSetting.GetMemoVisibility(),
|
||||
Locale: generalSetting.GetLocale(),
|
||||
Theme: generalSetting.GetTheme(),
|
||||
}
|
||||
|
||||
// Apply updates for fields specified in the update mask
|
||||
incomingGeneral := request.Setting.GetGeneralSetting()
|
||||
for _, field := range request.UpdateMask.Paths {
|
||||
switch field {
|
||||
case "memo_visibility":
|
||||
updatedGeneral.MemoVisibility = incomingGeneral.MemoVisibility
|
||||
case "theme":
|
||||
updatedGeneral.Theme = incomingGeneral.Theme
|
||||
case "locale":
|
||||
updatedGeneral.Locale = incomingGeneral.Locale
|
||||
default:
|
||||
// Ignore unsupported fields
|
||||
generalSetting := &storepb.GeneralUserSetting{}
|
||||
if existingUserSetting != nil {
|
||||
// Start with existing general setting values.
|
||||
generalSetting = existingUserSetting.GetGeneral()
|
||||
}
|
||||
}
|
||||
|
||||
// Create the updated setting
|
||||
updatedSetting := &v1pb.UserSetting{
|
||||
Name: request.Setting.Name,
|
||||
Value: &v1pb.UserSetting_GeneralSetting_{
|
||||
GeneralSetting: updatedGeneral,
|
||||
},
|
||||
updatedGeneral := &v1pb.UserSetting_GeneralSetting{
|
||||
MemoVisibility: generalSetting.GetMemoVisibility(),
|
||||
Locale: generalSetting.GetLocale(),
|
||||
Theme: generalSetting.GetTheme(),
|
||||
}
|
||||
|
||||
incomingGeneral := request.Setting.GetGeneralSetting()
|
||||
if incomingGeneral == nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "general setting is required")
|
||||
}
|
||||
for _, field := range request.UpdateMask.Paths {
|
||||
switch field {
|
||||
case "memo_visibility":
|
||||
updatedGeneral.MemoVisibility = incomingGeneral.MemoVisibility
|
||||
case "theme":
|
||||
updatedGeneral.Theme = incomingGeneral.Theme
|
||||
case "locale":
|
||||
updatedGeneral.Locale = incomingGeneral.Locale
|
||||
default:
|
||||
// Ignore unsupported fields.
|
||||
}
|
||||
}
|
||||
|
||||
updatedSetting = &v1pb.UserSetting{
|
||||
Name: request.Setting.Name,
|
||||
Value: &v1pb.UserSetting_GeneralSetting_{
|
||||
GeneralSetting: updatedGeneral,
|
||||
},
|
||||
}
|
||||
case storepb.UserSetting_TAGS:
|
||||
if len(request.UpdateMask.Paths) != 1 || request.UpdateMask.Paths[0] != "tags" {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "tags setting only supports update_mask [\"tags\"]")
|
||||
}
|
||||
incomingTags := request.Setting.GetTagsSetting()
|
||||
if incomingTags == nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "tags setting is required")
|
||||
}
|
||||
normalizedTags, err := validateAndNormalizeUserTagsSetting(incomingTags)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "invalid tags setting: %v", err)
|
||||
}
|
||||
updatedSetting = &v1pb.UserSetting{
|
||||
Name: request.Setting.Name,
|
||||
Value: &v1pb.UserSetting_TagsSetting_{
|
||||
TagsSetting: normalizedTags,
|
||||
},
|
||||
}
|
||||
default:
|
||||
return nil, status.Errorf(codes.InvalidArgument, "setting type %s should not be updated via UpdateUserSetting", storeKey.String())
|
||||
}
|
||||
|
||||
// Convert API setting to store setting
|
||||
|
|
@ -493,23 +520,34 @@ func (s *APIV1Service) ListUserSettings(ctx context.Context, request *v1pb.ListU
|
|||
}
|
||||
}
|
||||
|
||||
// If no general setting exists, add a default one
|
||||
hasGeneral := false
|
||||
hasTags := false
|
||||
for _, setting := range settings {
|
||||
if setting.GetGeneralSetting() != nil {
|
||||
hasGeneral = true
|
||||
break
|
||||
}
|
||||
if setting.GetTagsSetting() != nil {
|
||||
hasTags = true
|
||||
}
|
||||
}
|
||||
if !hasGeneral {
|
||||
defaultGeneral := &v1pb.UserSetting{
|
||||
Name: fmt.Sprintf("users/%d/settings/general", userID),
|
||||
Name: fmt.Sprintf("users/%d/settings/%s", userID, convertSettingKeyFromStore(storepb.UserSetting_GENERAL)),
|
||||
Value: &v1pb.UserSetting_GeneralSetting_{
|
||||
GeneralSetting: getDefaultUserGeneralSetting(),
|
||||
},
|
||||
}
|
||||
settings = append([]*v1pb.UserSetting{defaultGeneral}, settings...)
|
||||
}
|
||||
if !hasTags {
|
||||
defaultTags := &v1pb.UserSetting{
|
||||
Name: fmt.Sprintf("users/%d/settings/%s", userID, convertSettingKeyFromStore(storepb.UserSetting_TAGS)),
|
||||
Value: &v1pb.UserSetting_TagsSetting_{
|
||||
TagsSetting: getDefaultUserTagsSetting(),
|
||||
},
|
||||
}
|
||||
settings = append(settings, defaultTags)
|
||||
}
|
||||
|
||||
response := &v1pb.ListUserSettingsResponse{
|
||||
Settings: settings,
|
||||
|
|
@ -999,6 +1037,8 @@ func convertSettingKeyToStore(key string) (storepb.UserSetting_Key, error) {
|
|||
return storepb.UserSetting_GENERAL, nil
|
||||
case v1pb.UserSetting_Key_name[int32(v1pb.UserSetting_WEBHOOKS)]:
|
||||
return storepb.UserSetting_WEBHOOKS, nil
|
||||
case v1pb.UserSetting_Key_name[int32(v1pb.UserSetting_TAGS)]:
|
||||
return storepb.UserSetting_TAGS, nil
|
||||
default:
|
||||
return storepb.UserSetting_KEY_UNSPECIFIED, errors.Errorf("unknown setting key: %s", key)
|
||||
}
|
||||
|
|
@ -1013,6 +1053,8 @@ func convertSettingKeyFromStore(key storepb.UserSetting_Key) string {
|
|||
return "SHORTCUTS" // Not defined in API proto
|
||||
case storepb.UserSetting_WEBHOOKS:
|
||||
return v1pb.UserSetting_Key_name[int32(v1pb.UserSetting_WEBHOOKS)]
|
||||
case storepb.UserSetting_TAGS:
|
||||
return v1pb.UserSetting_Key_name[int32(v1pb.UserSetting_TAGS)]
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
|
|
@ -1034,6 +1076,10 @@ func convertUserSettingFromStore(storeSetting *storepb.UserSetting, userID int32
|
|||
Webhooks: []*v1pb.UserWebhook{},
|
||||
},
|
||||
}
|
||||
case storepb.UserSetting_TAGS:
|
||||
setting.Value = &v1pb.UserSetting_TagsSetting_{
|
||||
TagsSetting: getDefaultUserTagsSetting(),
|
||||
}
|
||||
default:
|
||||
// Default to general setting
|
||||
setting.Value = &v1pb.UserSetting_GeneralSetting_{
|
||||
|
|
@ -1079,6 +1125,19 @@ func convertUserSettingFromStore(storeSetting *storepb.UserSetting, userID int32
|
|||
Webhooks: apiWebhooks,
|
||||
},
|
||||
}
|
||||
case storepb.UserSetting_TAGS:
|
||||
tags := storeSetting.GetTags()
|
||||
apiTags := make(map[string]*v1pb.UserSetting_TagMetadata, len(tags.GetTags()))
|
||||
for tag, metadata := range tags.GetTags() {
|
||||
apiTags[tag] = &v1pb.UserSetting_TagMetadata{
|
||||
BackgroundColor: metadata.GetBackgroundColor(),
|
||||
}
|
||||
}
|
||||
setting.Value = &v1pb.UserSetting_TagsSetting_{
|
||||
TagsSetting: &v1pb.UserSetting_TagsSetting{
|
||||
Tags: apiTags,
|
||||
},
|
||||
}
|
||||
default:
|
||||
// Default to general setting if unknown key
|
||||
setting.Value = &v1pb.UserSetting_GeneralSetting_{
|
||||
|
|
@ -1128,6 +1187,22 @@ func convertUserSettingToStore(apiSetting *v1pb.UserSetting, userID int32, key s
|
|||
} else {
|
||||
return nil, errors.Errorf("webhooks setting is required")
|
||||
}
|
||||
case storepb.UserSetting_TAGS:
|
||||
if tags := apiSetting.GetTagsSetting(); tags != nil {
|
||||
storeTags := make(map[string]*storepb.TagMetadata, len(tags.GetTags()))
|
||||
for tag, metadata := range tags.GetTags() {
|
||||
storeTags[tag] = &storepb.TagMetadata{
|
||||
BackgroundColor: metadata.GetBackgroundColor(),
|
||||
}
|
||||
}
|
||||
storeSetting.Value = &storepb.UserSetting_Tags{
|
||||
Tags: &storepb.TagsUserSetting{
|
||||
Tags: storeTags,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
return nil, errors.Errorf("tags setting is required")
|
||||
}
|
||||
default:
|
||||
return nil, errors.Errorf("unsupported setting key: %v", key)
|
||||
}
|
||||
|
|
@ -1145,6 +1220,59 @@ func extractWebhookIDFromName(name string) string {
|
|||
return ""
|
||||
}
|
||||
|
||||
func validateAndNormalizeUserTagsSetting(tagsSetting *v1pb.UserSetting_TagsSetting) (*v1pb.UserSetting_TagsSetting, error) {
|
||||
normalized := &v1pb.UserSetting_TagsSetting{
|
||||
Tags: make(map[string]*v1pb.UserSetting_TagMetadata, len(tagsSetting.GetTags())),
|
||||
}
|
||||
for tag, metadata := range tagsSetting.GetTags() {
|
||||
if strings.TrimSpace(tag) == "" {
|
||||
return nil, errors.New("tag key cannot be empty")
|
||||
}
|
||||
if metadata == nil {
|
||||
return nil, errors.Errorf("tag metadata is required for %q", tag)
|
||||
}
|
||||
backgroundColor := metadata.GetBackgroundColor()
|
||||
if backgroundColor == nil {
|
||||
return nil, errors.Errorf("background_color is required for %q", tag)
|
||||
}
|
||||
if err := validateColor(backgroundColor); err != nil {
|
||||
return nil, errors.Wrapf(err, "background_color for %q", tag)
|
||||
}
|
||||
normalized.Tags[tag] = &v1pb.UserSetting_TagMetadata{
|
||||
BackgroundColor: backgroundColor,
|
||||
}
|
||||
}
|
||||
return normalized, nil
|
||||
}
|
||||
|
||||
func validateColor(color *colorpb.Color) error {
|
||||
if err := validateColorComponent("red", color.GetRed()); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateColorComponent("green", color.GetGreen()); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateColorComponent("blue", color.GetBlue()); err != nil {
|
||||
return err
|
||||
}
|
||||
if alpha := color.GetAlpha(); alpha != nil {
|
||||
if err := validateColorComponent("alpha", alpha.GetValue()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateColorComponent(name string, value float32) error {
|
||||
if math.IsNaN(float64(value)) || math.IsInf(float64(value), 0) {
|
||||
return errors.Errorf("%s must be a finite number", name)
|
||||
}
|
||||
if value < 0 || value > 1 {
|
||||
return errors.Errorf("%s must be between 0 and 1", name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// extractUsernameFromFilter extracts username from the filter string using CEL.
|
||||
// Supported filter format: "username == 'steven'"
|
||||
// Returns the username value and an error if the filter format is invalid.
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
colorpb "google.golang.org/genproto/googleapis/type/color"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
storepb "github.com/usememos/memos/proto/gen/store"
|
||||
|
|
@ -104,6 +105,48 @@ func TestUserSettingUpsertUpdate(t *testing.T) {
|
|||
ts.Close()
|
||||
}
|
||||
|
||||
func TestUserSettingTags(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
ts := NewTestingStore(ctx, t)
|
||||
user, err := createTestingHostUser(ctx, ts)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = ts.UpsertUserSetting(ctx, &storepb.UserSetting{
|
||||
UserId: user.ID,
|
||||
Key: storepb.UserSetting_TAGS,
|
||||
Value: &storepb.UserSetting_Tags{
|
||||
Tags: &storepb.TagsUserSetting{
|
||||
Tags: map[string]*storepb.TagMetadata{
|
||||
"bug": {
|
||||
BackgroundColor: &colorpb.Color{
|
||||
Red: 0.1,
|
||||
Green: 0.2,
|
||||
Blue: 0.3,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
setting, err := ts.GetUserSetting(ctx, &store.FindUserSetting{
|
||||
UserID: &user.ID,
|
||||
Key: storepb.UserSetting_TAGS,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, setting)
|
||||
require.Contains(t, setting.GetTags().Tags, "bug")
|
||||
require.InDelta(t, 0.1, setting.GetTags().Tags["bug"].GetBackgroundColor().GetRed(), 0.0001)
|
||||
|
||||
list, err := ts.ListUserSettings(ctx, &store.FindUserSetting{UserID: &user.ID})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, list, 1)
|
||||
|
||||
ts.Close()
|
||||
}
|
||||
|
||||
func TestUserSettingRefreshTokens(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
|
|
|
|||
|
|
@ -431,6 +431,12 @@ func convertUserSettingFromRaw(raw *UserSetting) (*storepb.UserSetting, error) {
|
|||
return nil, err
|
||||
}
|
||||
userSetting.Value = &storepb.UserSetting_Webhooks{Webhooks: webhooksUserSetting}
|
||||
case storepb.UserSetting_TAGS:
|
||||
tagsUserSetting := &storepb.TagsUserSetting{}
|
||||
if err := protojsonUnmarshaler.Unmarshal([]byte(raw.Value), tagsUserSetting); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userSetting.Value = &storepb.UserSetting_Tags{Tags: tagsUserSetting}
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
|
|
@ -479,6 +485,13 @@ func convertUserSettingToRaw(userSetting *storepb.UserSetting) (*UserSetting, er
|
|||
return nil, err
|
||||
}
|
||||
raw.Value = string(value)
|
||||
case storepb.UserSetting_TAGS:
|
||||
tagsUserSetting := userSetting.GetTags()
|
||||
value, err := protojson.Marshal(tagsUserSetting)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
raw.Value = string(value)
|
||||
default:
|
||||
return nil, errors.Errorf("unsupported user setting key: %v", userSetting.Key)
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,204 @@
|
|||
// Copyright 2025 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
|
||||
// @generated from file google/type/color.proto (package google.type, syntax proto3)
|
||||
/* eslint-disable */
|
||||
|
||||
import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2";
|
||||
import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2";
|
||||
import { file_google_protobuf_wrappers } from "@bufbuild/protobuf/wkt";
|
||||
import type { Message } from "@bufbuild/protobuf";
|
||||
|
||||
/**
|
||||
* Describes the file google/type/color.proto.
|
||||
*/
|
||||
export const file_google_type_color: GenFile = /*@__PURE__*/
|
||||
fileDesc("Chdnb29nbGUvdHlwZS9jb2xvci5wcm90bxILZ29vZ2xlLnR5cGUiXQoFQ29sb3ISCwoDcmVkGAEgASgCEg0KBWdyZWVuGAIgASgCEgwKBGJsdWUYAyABKAISKgoFYWxwaGEYBCABKAsyGy5nb29nbGUucHJvdG9idWYuRmxvYXRWYWx1ZUKlAQoPY29tLmdvb2dsZS50eXBlQgpDb2xvclByb3RvUAFaNmdvb2dsZS5nb2xhbmcub3JnL2dlbnByb3RvL2dvb2dsZWFwaXMvdHlwZS9jb2xvcjtjb2xvcvgBAaICA0dUWKoCC0dvb2dsZS5UeXBlygILR29vZ2xlXFR5cGXiAhdHb29nbGVcVHlwZVxHUEJNZXRhZGF0YeoCDEdvb2dsZTo6VHlwZWIGcHJvdG8z", [file_google_protobuf_wrappers]);
|
||||
|
||||
/**
|
||||
* Represents a color in the RGBA color space. This representation is designed
|
||||
* for simplicity of conversion to/from color representations in various
|
||||
* languages over compactness. For example, the fields of this representation
|
||||
* can be trivially provided to the constructor of `java.awt.Color` in Java; it
|
||||
* can also be trivially provided to UIColor's `+colorWithRed:green:blue:alpha`
|
||||
* method in iOS; and, with just a little work, it can be easily formatted into
|
||||
* a CSS `rgba()` string in JavaScript.
|
||||
*
|
||||
* This reference page doesn't carry information about the absolute color
|
||||
* space
|
||||
* that should be used to interpret the RGB value (e.g. sRGB, Adobe RGB,
|
||||
* DCI-P3, BT.2020, etc.). By default, applications should assume the sRGB color
|
||||
* space.
|
||||
*
|
||||
* When color equality needs to be decided, implementations, unless
|
||||
* documented otherwise, treat two colors as equal if all their red,
|
||||
* green, blue, and alpha values each differ by at most 1e-5.
|
||||
*
|
||||
* Example (Java):
|
||||
*
|
||||
* import com.google.type.Color;
|
||||
*
|
||||
* // ...
|
||||
* public static java.awt.Color fromProto(Color protocolor) {
|
||||
* float alpha = protocolor.hasAlpha()
|
||||
* ? protocolor.getAlpha().getValue()
|
||||
* : 1.0;
|
||||
*
|
||||
* return new java.awt.Color(
|
||||
* protocolor.getRed(),
|
||||
* protocolor.getGreen(),
|
||||
* protocolor.getBlue(),
|
||||
* alpha);
|
||||
* }
|
||||
*
|
||||
* public static Color toProto(java.awt.Color color) {
|
||||
* float red = (float) color.getRed();
|
||||
* float green = (float) color.getGreen();
|
||||
* float blue = (float) color.getBlue();
|
||||
* float denominator = 255.0;
|
||||
* Color.Builder resultBuilder =
|
||||
* Color
|
||||
* .newBuilder()
|
||||
* .setRed(red / denominator)
|
||||
* .setGreen(green / denominator)
|
||||
* .setBlue(blue / denominator);
|
||||
* int alpha = color.getAlpha();
|
||||
* if (alpha != 255) {
|
||||
* result.setAlpha(
|
||||
* FloatValue
|
||||
* .newBuilder()
|
||||
* .setValue(((float) alpha) / denominator)
|
||||
* .build());
|
||||
* }
|
||||
* return resultBuilder.build();
|
||||
* }
|
||||
* // ...
|
||||
*
|
||||
* Example (iOS / Obj-C):
|
||||
*
|
||||
* // ...
|
||||
* static UIColor* fromProto(Color* protocolor) {
|
||||
* float red = [protocolor red];
|
||||
* float green = [protocolor green];
|
||||
* float blue = [protocolor blue];
|
||||
* FloatValue* alpha_wrapper = [protocolor alpha];
|
||||
* float alpha = 1.0;
|
||||
* if (alpha_wrapper != nil) {
|
||||
* alpha = [alpha_wrapper value];
|
||||
* }
|
||||
* return [UIColor colorWithRed:red green:green blue:blue alpha:alpha];
|
||||
* }
|
||||
*
|
||||
* static Color* toProto(UIColor* color) {
|
||||
* CGFloat red, green, blue, alpha;
|
||||
* if (![color getRed:&red green:&green blue:&blue alpha:&alpha]) {
|
||||
* return nil;
|
||||
* }
|
||||
* Color* result = [[Color alloc] init];
|
||||
* [result setRed:red];
|
||||
* [result setGreen:green];
|
||||
* [result setBlue:blue];
|
||||
* if (alpha <= 0.9999) {
|
||||
* [result setAlpha:floatWrapperWithValue(alpha)];
|
||||
* }
|
||||
* [result autorelease];
|
||||
* return result;
|
||||
* }
|
||||
* // ...
|
||||
*
|
||||
* Example (JavaScript):
|
||||
*
|
||||
* // ...
|
||||
*
|
||||
* var protoToCssColor = function(rgb_color) {
|
||||
* var redFrac = rgb_color.red || 0.0;
|
||||
* var greenFrac = rgb_color.green || 0.0;
|
||||
* var blueFrac = rgb_color.blue || 0.0;
|
||||
* var red = Math.floor(redFrac * 255);
|
||||
* var green = Math.floor(greenFrac * 255);
|
||||
* var blue = Math.floor(blueFrac * 255);
|
||||
*
|
||||
* if (!('alpha' in rgb_color)) {
|
||||
* return rgbToCssColor(red, green, blue);
|
||||
* }
|
||||
*
|
||||
* var alphaFrac = rgb_color.alpha.value || 0.0;
|
||||
* var rgbParams = [red, green, blue].join(',');
|
||||
* return ['rgba(', rgbParams, ',', alphaFrac, ')'].join('');
|
||||
* };
|
||||
*
|
||||
* var rgbToCssColor = function(red, green, blue) {
|
||||
* var rgbNumber = new Number((red << 16) | (green << 8) | blue);
|
||||
* var hexString = rgbNumber.toString(16);
|
||||
* var missingZeros = 6 - hexString.length;
|
||||
* var resultBuilder = ['#'];
|
||||
* for (var i = 0; i < missingZeros; i++) {
|
||||
* resultBuilder.push('0');
|
||||
* }
|
||||
* resultBuilder.push(hexString);
|
||||
* return resultBuilder.join('');
|
||||
* };
|
||||
*
|
||||
* // ...
|
||||
*
|
||||
* @generated from message google.type.Color
|
||||
*/
|
||||
export type Color = Message<"google.type.Color"> & {
|
||||
/**
|
||||
* The amount of red in the color as a value in the interval [0, 1].
|
||||
*
|
||||
* @generated from field: float red = 1;
|
||||
*/
|
||||
red: number;
|
||||
|
||||
/**
|
||||
* The amount of green in the color as a value in the interval [0, 1].
|
||||
*
|
||||
* @generated from field: float green = 2;
|
||||
*/
|
||||
green: number;
|
||||
|
||||
/**
|
||||
* The amount of blue in the color as a value in the interval [0, 1].
|
||||
*
|
||||
* @generated from field: float blue = 3;
|
||||
*/
|
||||
blue: number;
|
||||
|
||||
/**
|
||||
* The fraction of this color that should be applied to the pixel. That is,
|
||||
* the final pixel color is defined by the equation:
|
||||
*
|
||||
* `pixel color = alpha * (this color) + (1.0 - alpha) * (background color)`
|
||||
*
|
||||
* This means that a value of 1.0 corresponds to a solid color, whereas
|
||||
* a value of 0.0 corresponds to a completely transparent color. This
|
||||
* uses a wrapper message rather than a simple float scalar so that it is
|
||||
* possible to distinguish between a default value and the value being unset.
|
||||
* If omitted, this color object is rendered as a solid color
|
||||
* (as if the alpha value had been explicitly given a value of 1.0).
|
||||
*
|
||||
* @generated from field: google.protobuf.FloatValue alpha = 4;
|
||||
*/
|
||||
alpha?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message google.type.Color.
|
||||
* Use `create(ColorSchema)` to create a new message.
|
||||
*/
|
||||
export const ColorSchema: GenMessage<Color> = /*@__PURE__*/
|
||||
messageDesc(file_google_type_color, 0);
|
||||
|
||||
Loading…
Reference in New Issue