feat: impl resources list page

pull/2227/head
Steven 1 year ago
parent 4424c8a231
commit fb1490c183

@ -2,11 +2,13 @@ package v2
import (
"context"
"time"
apiv2pb "github.com/usememos/memos/proto/gen/api/v2"
"github.com/usememos/memos/store"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"
)
type ResourceService struct {
@ -28,7 +30,8 @@ func (s *ResourceService) ListResources(ctx context.Context, _ *apiv2pb.ListReso
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
}
resources, err := s.Store.ListResources(ctx, &store.FindResource{
CreatorID: &user.ID,
CreatorID: &user.ID,
HasRelatedMemo: true,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list tags: %v", err)
@ -44,7 +47,7 @@ func (s *ResourceService) ListResources(ctx context.Context, _ *apiv2pb.ListReso
func convertResourceFromStore(resource *store.Resource) *apiv2pb.Resource {
return &apiv2pb.Resource{
Id: resource.ID,
CreatedTs: resource.CreatedTs,
CreatedTs: timestamppb.New(time.Unix(resource.CreatedTs, 0)),
Filename: resource.Filename,
ExternalLink: resource.ExternalLink,
Type: resource.Type,

@ -3,6 +3,7 @@ syntax = "proto3";
package memos.api.v2;
import "google/api/annotations.proto";
import "google/protobuf/timestamp.proto";
option go_package = "gen/api/v2";
@ -14,7 +15,7 @@ service ResourceService {
message Resource {
int32 id = 1;
int64 created_ts = 2;
google.protobuf.Timestamp created_ts = 2;
string filename = 3;
string external_link = 4;
string type = 5;

@ -257,7 +257,7 @@
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| id | [int32](#int32) | | |
| created_ts | [int64](#int64) | | |
| created_ts | [google.protobuf.Timestamp](#google-protobuf-Timestamp) | | |
| filename | [string](#string) | | |
| external_link | [string](#string) | | |
| type | [string](#string) | | |

@ -10,6 +10,7 @@ import (
_ "google.golang.org/genproto/googleapis/api/annotations"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
reflect "reflect"
sync "sync"
)
@ -26,13 +27,13 @@ type Resource struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
CreatedTs int64 `protobuf:"varint,2,opt,name=created_ts,json=createdTs,proto3" json:"created_ts,omitempty"`
Filename string `protobuf:"bytes,3,opt,name=filename,proto3" json:"filename,omitempty"`
ExternalLink string `protobuf:"bytes,4,opt,name=external_link,json=externalLink,proto3" json:"external_link,omitempty"`
Type string `protobuf:"bytes,5,opt,name=type,proto3" json:"type,omitempty"`
Size int64 `protobuf:"varint,6,opt,name=size,proto3" json:"size,omitempty"`
RelatedMemoId *int32 `protobuf:"varint,7,opt,name=related_memo_id,json=relatedMemoId,proto3,oneof" json:"related_memo_id,omitempty"`
Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
CreatedTs *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=created_ts,json=createdTs,proto3" json:"created_ts,omitempty"`
Filename string `protobuf:"bytes,3,opt,name=filename,proto3" json:"filename,omitempty"`
ExternalLink string `protobuf:"bytes,4,opt,name=external_link,json=externalLink,proto3" json:"external_link,omitempty"`
Type string `protobuf:"bytes,5,opt,name=type,proto3" json:"type,omitempty"`
Size int64 `protobuf:"varint,6,opt,name=size,proto3" json:"size,omitempty"`
RelatedMemoId *int32 `protobuf:"varint,7,opt,name=related_memo_id,json=relatedMemoId,proto3,oneof" json:"related_memo_id,omitempty"`
}
func (x *Resource) Reset() {
@ -74,11 +75,11 @@ func (x *Resource) GetId() int32 {
return 0
}
func (x *Resource) GetCreatedTs() int64 {
func (x *Resource) GetCreatedTs() *timestamppb.Timestamp {
if x != nil {
return x.CreatedTs
}
return 0
return nil
}
func (x *Resource) GetFilename() string {
@ -208,48 +209,52 @@ var file_api_v2_resource_service_proto_rawDesc = []byte{
0x65, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12,
0x0c, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x1a, 0x1c, 0x67,
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61,
0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xe3, 0x01, 0x0a, 0x08,
0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01,
0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61,
0x74, 0x65, 0x64, 0x5f, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x72,
0x65, 0x61, 0x74, 0x65, 0x64, 0x54, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x6e,
0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x6e,
0x61, 0x6d, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f,
0x6c, 0x69, 0x6e, 0x6b, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x78, 0x74, 0x65,
0x72, 0x6e, 0x61, 0x6c, 0x4c, 0x69, 0x6e, 0x6b, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65,
0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04,
0x73, 0x69, 0x7a, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65,
0x12, 0x2b, 0x0a, 0x0f, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x6d, 0x65, 0x6d, 0x6f,
0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, 0x0d, 0x72, 0x65, 0x6c,
0x61, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x6d, 0x6f, 0x49, 0x64, 0x88, 0x01, 0x01, 0x42, 0x12, 0x0a,
0x10, 0x5f, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x6d, 0x65, 0x6d, 0x6f, 0x5f, 0x69,
0x64, 0x22, 0x16, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x4d, 0x0a, 0x15, 0x4c, 0x69, 0x73,
0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x12, 0x34, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18,
0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70,
0x69, 0x2e, 0x76, 0x32, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72,
0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x32, 0x86, 0x01, 0x0a, 0x0f, 0x52, 0x65, 0x73,
0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x73, 0x0a, 0x0d,
0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x22, 0x2e,
0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73,
0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x1a, 0x23, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32,
0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x19, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x13, 0x12, 0x11,
0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
0x73, 0x42, 0xac, 0x01, 0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x42, 0x14, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x30,
0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x75, 0x73, 0x65, 0x6d, 0x65,
0x6d, 0x6f, 0x73, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f,
0x67, 0x65, 0x6e, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x3b, 0x61, 0x70, 0x69, 0x76, 0x32,
0xa2, 0x02, 0x03, 0x4d, 0x41, 0x58, 0xaa, 0x02, 0x0c, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x41,
0x70, 0x69, 0x2e, 0x56, 0x32, 0xca, 0x02, 0x0c, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x5c, 0x41, 0x70,
0x69, 0x5c, 0x56, 0x32, 0xe2, 0x02, 0x18, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x5c, 0x41, 0x70, 0x69,
0x5c, 0x56, 0x32, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea,
0x02, 0x0e, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x3a, 0x3a, 0x41, 0x70, 0x69, 0x3a, 0x3a, 0x56, 0x32,
0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f,
0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d,
0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xff, 0x01, 0x0a,
0x08, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18,
0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65,
0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e,
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74,
0x65, 0x64, 0x54, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x6e, 0x61, 0x6d, 0x65,
0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x6e, 0x61, 0x6d, 0x65,
0x12, 0x23, 0x0a, 0x0d, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x6c, 0x69, 0x6e,
0x6b, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61,
0x6c, 0x4c, 0x69, 0x6e, 0x6b, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x05, 0x20,
0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a,
0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x2b, 0x0a,
0x0f, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x6d, 0x65, 0x6d, 0x6f, 0x5f, 0x69, 0x64,
0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, 0x0d, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x65,
0x64, 0x4d, 0x65, 0x6d, 0x6f, 0x49, 0x64, 0x88, 0x01, 0x01, 0x42, 0x12, 0x0a, 0x10, 0x5f, 0x72,
0x65, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x6d, 0x65, 0x6d, 0x6f, 0x5f, 0x69, 0x64, 0x22, 0x16,
0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x4d, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65,
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
0x34, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03,
0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76,
0x32, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f,
0x75, 0x72, 0x63, 0x65, 0x73, 0x32, 0x86, 0x01, 0x0a, 0x0f, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72,
0x63, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x73, 0x0a, 0x0d, 0x4c, 0x69, 0x73,
0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x22, 0x2e, 0x6d, 0x65, 0x6d,
0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65,
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23,
0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69,
0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x22, 0x19, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x13, 0x12, 0x11, 0x2f, 0x61, 0x70,
0x69, 0x2f, 0x76, 0x32, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x42, 0xac,
0x01, 0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x76, 0x32, 0x42, 0x14, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x72,
0x76, 0x69, 0x63, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x30, 0x67, 0x69, 0x74,
0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x75, 0x73, 0x65, 0x6d, 0x65, 0x6d, 0x6f, 0x73,
0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x65, 0x6e,
0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x3b, 0x61, 0x70, 0x69, 0x76, 0x32, 0xa2, 0x02, 0x03,
0x4d, 0x41, 0x58, 0xaa, 0x02, 0x0c, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x41, 0x70, 0x69, 0x2e,
0x56, 0x32, 0xca, 0x02, 0x0c, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x5c, 0x41, 0x70, 0x69, 0x5c, 0x56,
0x32, 0xe2, 0x02, 0x18, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x5c, 0x41, 0x70, 0x69, 0x5c, 0x56, 0x32,
0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x0e, 0x4d,
0x65, 0x6d, 0x6f, 0x73, 0x3a, 0x3a, 0x41, 0x70, 0x69, 0x3a, 0x3a, 0x56, 0x32, 0x62, 0x06, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@ -269,16 +274,18 @@ var file_api_v2_resource_service_proto_goTypes = []interface{}{
(*Resource)(nil), // 0: memos.api.v2.Resource
(*ListResourcesRequest)(nil), // 1: memos.api.v2.ListResourcesRequest
(*ListResourcesResponse)(nil), // 2: memos.api.v2.ListResourcesResponse
(*timestamppb.Timestamp)(nil), // 3: google.protobuf.Timestamp
}
var file_api_v2_resource_service_proto_depIdxs = []int32{
0, // 0: memos.api.v2.ListResourcesResponse.resources:type_name -> memos.api.v2.Resource
1, // 1: memos.api.v2.ResourceService.ListResources:input_type -> memos.api.v2.ListResourcesRequest
2, // 2: memos.api.v2.ResourceService.ListResources:output_type -> memos.api.v2.ListResourcesResponse
2, // [2:3] is the sub-list for method output_type
1, // [1:2] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
3, // 0: memos.api.v2.Resource.created_ts:type_name -> google.protobuf.Timestamp
0, // 1: memos.api.v2.ListResourcesResponse.resources:type_name -> memos.api.v2.Resource
1, // 2: memos.api.v2.ResourceService.ListResources:input_type -> memos.api.v2.ListResourcesRequest
2, // 3: memos.api.v2.ResourceService.ListResources:output_type -> memos.api.v2.ListResourcesResponse
3, // [3:4] is the sub-list for method output_type
2, // [2:3] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
}
func init() { file_api_v2_resource_service_proto_init() }

@ -28,13 +28,14 @@ type Resource struct {
}
type FindResource struct {
GetBlob bool
ID *int32
CreatorID *int32
Filename *string
MemoID *int32
Limit *int
Offset *int
GetBlob bool
ID *int32
CreatorID *int32
Filename *string
MemoID *int32
HasRelatedMemo bool
Limit *int
Offset *int
}
type UpdateResource struct {
@ -96,6 +97,9 @@ func (s *Store) ListResources(ctx context.Context, find *FindResource) ([]*Resou
if v := find.MemoID; v != nil {
where, args = append(where, "resource.id in (SELECT resource_id FROM memo_resource WHERE memo_id = ?)"), append(args, *v)
}
if find.HasRelatedMemo {
where = append(where, "memo_resource.memo_id IS NOT NULL")
}
fields := []string{"resource.id", "resource.filename", "resource.external_link", "resource.type", "resource.size", "resource.creator_id", "resource.created_ts", "resource.updated_ts", "internal_path"}
if find.GetBlob {
@ -110,7 +114,7 @@ func (s *Store) ListResources(ctx context.Context, find *FindResource) ([]*Resou
LEFT JOIN memo_resource ON resource.id = memo_resource.resource_id
WHERE %s
GROUP BY resource.id
ORDER BY resource.id DESC
ORDER BY resource.created_ts DESC
`, strings.Join(fields, ", "), strings.Join(where, " AND "))
if find.Limit != nil {
query = fmt.Sprintf("%s LIMIT %d", query, *find.Limit)

@ -1,6 +1,7 @@
import { Autocomplete, Button, Input, List, ListItem, Option, Select, Typography } from "@mui/joy";
import React, { useRef, useState } from "react";
import { toast } from "react-hot-toast";
import { Resource } from "@/types/proto/api/v2/resource_service_pb";
import { useTranslate } from "@/utils/i18n";
import { useResourceStore } from "../store/module";
import { generateDialog } from "./Dialog";

@ -1,3 +1,4 @@
import { Resource } from "@/types/proto/api/v2/resource_service_pb";
import Icon from "../Icon";
import ResourceIcon from "../ResourceIcon";
@ -23,7 +24,7 @@ const ResourceListView = (props: Props) => {
key={resource.id}
className="max-w-full flex flex-row justify-start items-center flex-nowrap gap-x-1 bg-gray-100 dark:bg-zinc-800 px-2 py-1 rounded text-gray-500"
>
<ResourceIcon resource={resource} className="!w-4 !h-auto !opacity-100" />
<ResourceIcon resource={resource} className="!w-4 !h-4 !opacity-100" />
<span className="text-sm max-w-[8rem] truncate">{resource.filename}</span>
<Icon.X
className="w-4 h-auto cursor-pointer opacity-60 hover:opacity-100"

@ -8,6 +8,7 @@ import { TAB_SPACE_WIDTH, UNKNOWN_ID } from "@/helpers/consts";
import { clearContentQueryParam } from "@/helpers/utils";
import { getMatchedNodes } from "@/labs/marked";
import { useFilterStore, useGlobalStore, useMemoStore, useResourceStore, useTagStore, useUserStore } from "@/store/module";
import { Resource } from "@/types/proto/api/v2/resource_service_pb";
import { useTranslate } from "@/utils/i18n";
import showCreateResourceDialog from "../CreateResourceDialog";
import Icon from "../Icon";

@ -1,5 +1,6 @@
import { Resource } from "@/types/proto/api/v2/resource_service_pb";
import { getResourceUrl } from "@/utils/resource";
import Icon from "./Icon";
import ResourceIcon from "./ResourceIcon";
interface Props {
resource: Resource;
@ -16,7 +17,7 @@ const MemoResource: React.FC<Props> = (props: Props) => {
return (
<>
<div className={`w-auto flex flex-row justify-start items-center hover:opacity-80 ${className}`}>
<div className={`w-auto flex flex-row justify-start items-center text-gray-500 dark:text-gray-400 hover:opacity-80 ${className}`}>
{resource.type.startsWith("audio") ? (
<>
<audio controls>
@ -25,8 +26,8 @@ const MemoResource: React.FC<Props> = (props: Props) => {
</>
) : (
<>
<Icon.FileText className="w-4 h-auto mr-1 text-gray-500" />
<span className="text-gray-500 text-sm max-w-[256px] truncate font-mono cursor-pointer" onClick={handlePreviewBtnClick}>
<ResourceIcon className="!w-4 !h-4 mr-1" resource={resource} />
<span className="text-sm max-w-[256px] truncate cursor-pointer" onClick={handlePreviewBtnClick}>
{resource.filename}
</span>
</>

@ -1,5 +1,6 @@
import classNames from "classnames";
import { absolutifyLink } from "@/helpers/utils";
import { Resource } from "@/types/proto/api/v2/resource_service_pb";
import { getResourceType, getResourceUrl } from "@/utils/resource";
import MemoResource from "./MemoResource";
import showPreviewImageDialog from "./PreviewImageDialog";
@ -42,7 +43,7 @@ const MemoResourceListView: React.FC<Props> = (props: Props) => {
<>
{imageResourceList.length > 0 &&
(imageResourceList.length === 1 ? (
<div className="mt-2 max-w-[90%] max-h-64 flex justify-center items-center shadow rounded overflow-hidden hide-scrollbar hover:shadow-md">
<div className="mt-2 max-w-[90%] max-h-64 flex justify-center items-center border rounded overflow-hidden hide-scrollbar hover:shadow-md">
<img
className="cursor-pointer min-h-full w-auto min-w-full object-cover"
src={getResourceUrl(imageResourceList[0])}
@ -63,7 +64,7 @@ const MemoResourceListView: React.FC<Props> = (props: Props) => {
return (
<SquareDiv
key={resource.id}
className="flex justify-center items-center shadow rounded overflow-hidden hide-scrollbar hover:shadow-md"
className="flex justify-center items-center border dark:border-zinc-900 rounded overflow-hidden hide-scrollbar hover:shadow-md"
>
<img
className="cursor-pointer min-h-full w-auto min-w-full object-cover"

@ -1,4 +1,5 @@
import { getDateTimeString } from "@/helpers/datetime";
import { Resource } from "@/types/proto/api/v2/resource_service_pb";
import ResourceIcon from "./ResourceIcon";
interface Props {

@ -1,5 +1,6 @@
import classNames from "classnames";
import React from "react";
import { Resource } from "@/types/proto/api/v2/resource_service_pb";
import { getResourceType, getResourceUrl } from "@/utils/resource";
import Icon from "./Icon";
import showPreviewImageDialog from "./PreviewImageDialog";
@ -18,38 +19,52 @@ const ResourceIcon = (props: Props) => {
const className = classNames("w-full h-auto", props.className);
const strokeWidth = props.strokeWidth;
switch (resourceType) {
case "image/*":
return (
<SquareDiv className={classNames(className, "flex items-center justify-center overflow-clip")}>
<img
className="max-w-full max-h-full object-cover shadow"
src={resource.externalLink ? resourceUrl : resourceUrl + "?thumbnail=1"}
onClick={() => showPreviewImageDialog(resourceUrl)}
/>
</SquareDiv>
);
case "video/*":
return <Icon.FileVideo2 strokeWidth={strokeWidth} className={classNames(className, "opacity-50")} />;
case "audio/*":
return <Icon.FileAudio strokeWidth={strokeWidth} className={classNames(className, "opacity-50")} />;
case "text/*":
return <Icon.FileText strokeWidth={strokeWidth} className={classNames(className, "opacity-50")} />;
case "application/epub+zip":
return <Icon.Book strokeWidth={strokeWidth} className={classNames(className, "opacity-50")} />;
case "application/pdf":
return <Icon.Book strokeWidth={strokeWidth} className={classNames(className, "opacity-50")} />;
case "application/msword":
return <Icon.FileEdit strokeWidth={strokeWidth} className={classNames(className, "opacity-50")} />;
case "application/msexcel":
return <Icon.SheetIcon strokeWidth={strokeWidth} className={classNames(className, "opacity-50")} />;
case "application/zip":
return <Icon.FileArchiveIcon strokeWidth={strokeWidth} className={classNames(className, "opacity-50")} />;
case "application/x-java-archive":
return <Icon.BinaryIcon strokeWidth={strokeWidth} className={classNames(className, "opacity-50")} />;
default:
return <Icon.File strokeWidth={strokeWidth} className={classNames(className, "opacity-50")} />;
const previewResource = () => {
window.open(resourceUrl);
};
if (resourceType === "image/*") {
return (
<SquareDiv className={classNames(className, "flex items-center justify-center overflow-clip")}>
<img
className="min-w-full min-h-full object-cover shadow"
src={resource.externalLink ? resourceUrl : resourceUrl + "?thumbnail=1"}
onClick={() => showPreviewImageDialog(resourceUrl)}
/>
</SquareDiv>
);
}
const getResourceIcon = () => {
switch (resourceType) {
case "video/*":
return <Icon.FileVideo2 strokeWidth={strokeWidth} className="w-full h-auto" />;
case "audio/*":
return <Icon.FileAudio strokeWidth={strokeWidth} className="w-full h-auto" />;
case "text/*":
return <Icon.FileText strokeWidth={strokeWidth} className="w-full h-auto" />;
case "application/epub+zip":
return <Icon.Book strokeWidth={strokeWidth} className="w-full h-auto" />;
case "application/pdf":
return <Icon.Book strokeWidth={strokeWidth} className="w-full h-auto" />;
case "application/msword":
return <Icon.FileEdit strokeWidth={strokeWidth} className="w-full h-auto" />;
case "application/msexcel":
return <Icon.SheetIcon strokeWidth={strokeWidth} className="w-full h-auto" />;
case "application/zip":
return <Icon.FileArchiveIcon onClick={previewResource} strokeWidth={strokeWidth} className="w-full h-auto" />;
case "application/x-java-archive":
return <Icon.BinaryIcon strokeWidth={strokeWidth} className="w-full h-auto" />;
default:
return <Icon.File strokeWidth={strokeWidth} className="w-full h-auto" />;
}
};
return (
<div onClick={previewResource} className={classNames(className, "max-w-[4rem] opacity-50")}>
{getResourceIcon()}
</div>
);
};
export default React.memo(ResourceIcon);

@ -64,7 +64,7 @@ const AccessTokenSection = () => {
Access Tokens
<LearnMore className="ml-2" url="https://usememos.com/docs/local-storage" />
</p>
<p className="text-sm text-gray-700">A list of all access tokens for your account.</p>
<p className="text-sm text-gray-700 dark:text-gray-500">A list of all access tokens for your account.</p>
</div>
<div className="mt-4 sm:mt-0">
<Button
@ -81,7 +81,7 @@ const AccessTokenSection = () => {
<div className="mt-2 flow-root">
<div className="overflow-x-auto">
<div className="inline-block min-w-full py-2 align-middle">
<table className="min-w-full divide-y divide-gray-300">
<table className="min-w-full divide-y divide-gray-300 dark:divide-gray-400">
<thead>
<tr>
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-400">
@ -101,7 +101,7 @@ const AccessTokenSection = () => {
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
<tbody className="divide-y divide-gray-200 dark:divide-gray-500">
{userAccessTokens.map((userAccessToken) => (
<tr key={userAccessToken.accessToken}>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-900 dark:text-gray-400 flex flex-row justify-start items-center gap-x-1">

@ -1,4 +1,5 @@
import axios from "axios";
import { Resource } from "@/types/proto/api/v2/resource_service_pb";
import { GetUserResponse } from "@/types/proto/api/v2/user_service_pb";
export function getSystemStatus() {
@ -143,17 +144,6 @@ export function getResourceList() {
return axios.get<Resource[]>("/api/v1/resource");
}
export function getResourceListWithLimit(resourceFind?: ResourceFind) {
const queryList = [];
if (resourceFind?.offset) {
queryList.push(`offset=${resourceFind.offset}`);
}
if (resourceFind?.limit) {
queryList.push(`limit=${resourceFind.limit}`);
}
return axios.get<Resource[]>(`/api/v1/resource?${queryList.join("&")}`);
}
export function createResource(resourceCreate: ResourceCreate) {
return axios.post<Resource>("/api/v1/resource", resourceCreate);
}

@ -57,7 +57,7 @@ export function getTimeString(t?: Date | number | string): string {
* - "pt-BR" locale: "30/01/2023 22:05:00"
* - "pl" locale: "30.01.2023, 22:05:00"
*/
export function getDateTimeString(t?: Date | number | string, locale = i18n.language): string {
export function getDateTimeString(t?: Date | number | string | any, locale = i18n.language): string {
const tsFromDate = getTimeStampByDate(t ? t : Date.now());
return new Date(tsFromDate).toLocaleDateString(locale, {

@ -1,29 +1,45 @@
import { useEffect } from "react";
import { toast } from "react-hot-toast";
import axios from "axios";
import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import Empty from "@/components/Empty";
import Icon from "@/components/Icon";
import MobileHeader from "@/components/MobileHeader";
import ResourceCard from "@/components/ResourceCard";
import ResourceIcon from "@/components/ResourceIcon";
import useLoading from "@/hooks/useLoading";
import { useResourceStore } from "@/store/module";
import { ListResourcesResponse, Resource } from "@/types/proto/api/v2/resource_service_pb";
import { useTranslate } from "@/utils/i18n";
const fetchAllResources = async () => {
const { data } = await axios.get<ListResourcesResponse>("/api/v2/resources");
return data.resources;
};
function groupResourcesByDate(resources: Resource[]) {
const grouped = new Map<number, Resource[]>();
resources.forEach((item) => {
const date = new Date(item.createdTs as any);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const timestamp = Date.UTC(year, month - 1, 1);
if (!grouped.has(timestamp)) {
grouped.set(timestamp, []);
}
grouped.get(timestamp)?.push(item);
});
return grouped;
}
const Resources = () => {
const t = useTranslate();
const loadingState = useLoading();
const resourceStore = useResourceStore();
const resources = resourceStore.state.resources;
const [resources, setResources] = useState<Resource[]>([]);
const groupedResources = groupResourcesByDate(resources);
useEffect(() => {
resourceStore
.fetchResourceList()
.then(() => {
loadingState.setFinish();
})
.catch((error) => {
console.error(error);
toast.error(error.response.data.message);
});
fetchAllResources().then((resources) => {
setResources(resources);
loadingState.setFinish();
});
}, []);
return (
@ -41,22 +57,51 @@ const Resources = () => {
<p className="w-full text-center text-base my-6 mt-8">{t("resource.fetching-data")}</p>
</div>
) : (
<div
className={
resources.length === 0
? "flex flex-col justify-start items-start w-full"
: "w-full h-auto grid grid-cols-2 md:grid-cols-4 gap-6"
}
>
<>
{resources.length === 0 ? (
<div className="w-full mt-8 mb-8 flex flex-col justify-center items-center italic">
<Empty />
<p className="mt-4 text-gray-600 dark:text-gray-400">{t("message.no-data")}</p>
</div>
) : (
resources.map((resource) => <ResourceCard key={resource.id} resource={resource}></ResourceCard>)
<div className={"w-full h-auto px-2 flex flex-col justify-start items-start gap-y-8"}>
{Array.from(groupedResources.entries()).map(([timestamp, resources]) => {
const date = new Date(timestamp);
return (
<div key={timestamp} className="w-full flex flex-row justify-start items-start">
<div className="w-16 sm:w-24 pt-4 sm:pl-4 flex flex-col justify-start items-start">
<span className="text-sm opacity-60">{date.getFullYear()}</span>
<span className="font-medium text-xl">{date.toLocaleString("default", { month: "short" })}</span>
</div>
<div className="w-full max-w-[calc(100%-4rem)] sm:max-w-[calc(100%-6rem)] flex flex-row justify-start items-start gap-4 flex-wrap">
{resources.map((resource) => {
return (
<div key={resource.id} className="w-auto h-auto flex flex-col justify-start items-start">
<div className="w-24 h-24 flex justify-center items-center sm:w-32 sm:h-32 border dark:border-zinc-900 overflow-clip rounded cursor-pointer hover:shadow hover:opacity-80">
<ResourceIcon resource={resource} strokeWidth={0.5} />
</div>
<div className="w-full flex flex-row justify-between items-center mt-1 px-1">
<div>
<p className="text-xs text-gray-400">{new Date(resource.createdTs as any).toLocaleDateString()}</p>
</div>
<Link
className="flex flex-row justify-start items-center text-gray-400 hover:underline hover:text-blue-600"
to={`/m/${resource.relatedMemoId}`}
target="_blank"
>
<span className="text-xs ml-0.5">#{resource.relatedMemoId}</span>
</Link>
</div>
</div>
);
})}
</div>
</div>
);
})}
</div>
)}
</div>
</>
)}
</div>
</div>

@ -1,17 +1,10 @@
import * as api from "@/helpers/api";
import { Resource } from "@/types/proto/api/v2/resource_service_pb";
import { useTranslate } from "@/utils/i18n";
import store, { useAppSelector } from "../";
import { deleteResource, patchResource, setResources } from "../reducer/resource";
import { useGlobalStore } from "./global";
const convertResponseModelResource = (resource: Resource): Resource => {
return {
...resource,
createdTs: resource.createdTs * 1000,
updatedTs: resource.updatedTs * 1000,
};
};
export const useResourceStore = () => {
const state = useAppSelector((state) => state.resource);
const t = useTranslate();
@ -24,14 +17,12 @@ export const useResourceStore = () => {
return store.getState().resource;
},
async fetchResourceList(): Promise<Resource[]> {
const { data } = await api.getResourceList();
const resourceList = data.map((m) => convertResponseModelResource(m));
const { data: resourceList } = await api.getResourceList();
store.dispatch(setResources(resourceList));
return resourceList;
},
async createResource(resourceCreate: ResourceCreate): Promise<Resource> {
const { data } = await api.createResource(resourceCreate);
const resource = convertResponseModelResource(data);
const { data: resource } = await api.createResource(resourceCreate);
const resourceList = state.resources;
store.dispatch(setResources([resource, ...resourceList]));
return resource;
@ -44,8 +35,7 @@ export const useResourceStore = () => {
const formData = new FormData();
formData.append("file", file, filename);
const { data } = await api.createResourceWithBlob(formData);
const resource = convertResponseModelResource(data);
const { data: resource } = await api.createResourceWithBlob(formData);
const resourceList = state.resources;
store.dispatch(setResources([resource, ...resourceList]));
return resource;
@ -60,8 +50,7 @@ export const useResourceStore = () => {
const formData = new FormData();
formData.append("file", file, filename);
const { data } = await api.createResourceWithBlob(formData);
const resource = convertResponseModelResource(data);
const { data: resource } = await api.createResourceWithBlob(formData);
newResourceList = [resource, ...newResourceList];
}
const resourceList = state.resources;
@ -73,8 +62,7 @@ export const useResourceStore = () => {
store.dispatch(deleteResource(id));
},
async patchResource(resourcePatch: ResourcePatch): Promise<Resource> {
const { data } = await api.patchResource(resourcePatch);
const resource = convertResponseModelResource(data);
const { data: resource } = await api.patchResource(resourcePatch);
store.dispatch(patchResource(resource));
return resource;
},

@ -1,5 +1,6 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { uniqBy } from "lodash-es";
import { Resource } from "@/types/proto/api/v2/resource_service_pb";
interface State {
resources: Resource[];

@ -16,7 +16,7 @@ interface Memo {
pinned: boolean;
creatorName: string;
resourceList: Resource[];
resourceList: any[];
relationList: MemoRelation[];
}

@ -1,17 +1,5 @@
type ResourceId = number;
interface Resource {
id: ResourceId;
createdTs: number;
updatedTs: number;
filename: string;
externalLink: string;
type: string;
size: string;
}
interface ResourceCreate {
filename: string;
externalLink: string;

@ -3,7 +3,7 @@
/* eslint-disable */
// @ts-nocheck
import type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from "@bufbuild/protobuf";
import type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage, Timestamp } from "@bufbuild/protobuf";
import { Message, proto3 } from "@bufbuild/protobuf";
/**
@ -16,9 +16,9 @@ export declare class Resource extends Message<Resource> {
id: number;
/**
* @generated from field: int64 created_ts = 2;
* @generated from field: google.protobuf.Timestamp created_ts = 2;
*/
createdTs: bigint;
createdTs?: Timestamp;
/**
* @generated from field: string filename = 3;

@ -3,7 +3,7 @@
/* eslint-disable */
// @ts-nocheck
import { proto3 } from "@bufbuild/protobuf";
import { proto3, Timestamp } from "@bufbuild/protobuf";
/**
* @generated from message memos.api.v2.Resource
@ -12,7 +12,7 @@ export const Resource = proto3.makeMessageType(
"memos.api.v2.Resource",
() => [
{ no: 1, name: "id", kind: "scalar", T: 5 /* ScalarType.INT32 */ },
{ no: 2, name: "created_ts", kind: "scalar", T: 3 /* ScalarType.INT64 */ },
{ no: 2, name: "created_ts", kind: "message", T: Timestamp },
{ no: 3, name: "filename", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 4, name: "external_link", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 5, name: "type", kind: "scalar", T: 9 /* ScalarType.STRING */ },

@ -1,7 +0,0 @@
interface ResourceProps {
resource: Resource;
handleCheckClick: () => void;
handleUncheckClick: () => void;
}
type ResourceItemType = ResourceProps;

@ -1,3 +1,5 @@
import { Resource } from "@/types/proto/api/v2/resource_service_pb";
export const getResourceUrl = (resource: Resource, withOrigin = true) => {
if (resource.externalLink) {
return resource.externalLink;

Loading…
Cancel
Save