mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
feat: impl resources list page
This commit is contained in:
@ -2,11 +2,13 @@ package v2
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
apiv2pb "github.com/usememos/memos/proto/gen/api/v2"
|
apiv2pb "github.com/usememos/memos/proto/gen/api/v2"
|
||||||
"github.com/usememos/memos/store"
|
"github.com/usememos/memos/store"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ResourceService struct {
|
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)
|
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
|
||||||
}
|
}
|
||||||
resources, err := s.Store.ListResources(ctx, &store.FindResource{
|
resources, err := s.Store.ListResources(ctx, &store.FindResource{
|
||||||
CreatorID: &user.ID,
|
CreatorID: &user.ID,
|
||||||
|
HasRelatedMemo: true,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Errorf(codes.Internal, "failed to list tags: %v", err)
|
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 {
|
func convertResourceFromStore(resource *store.Resource) *apiv2pb.Resource {
|
||||||
return &apiv2pb.Resource{
|
return &apiv2pb.Resource{
|
||||||
Id: resource.ID,
|
Id: resource.ID,
|
||||||
CreatedTs: resource.CreatedTs,
|
CreatedTs: timestamppb.New(time.Unix(resource.CreatedTs, 0)),
|
||||||
Filename: resource.Filename,
|
Filename: resource.Filename,
|
||||||
ExternalLink: resource.ExternalLink,
|
ExternalLink: resource.ExternalLink,
|
||||||
Type: resource.Type,
|
Type: resource.Type,
|
||||||
|
@ -3,6 +3,7 @@ syntax = "proto3";
|
|||||||
package memos.api.v2;
|
package memos.api.v2;
|
||||||
|
|
||||||
import "google/api/annotations.proto";
|
import "google/api/annotations.proto";
|
||||||
|
import "google/protobuf/timestamp.proto";
|
||||||
|
|
||||||
option go_package = "gen/api/v2";
|
option go_package = "gen/api/v2";
|
||||||
|
|
||||||
@ -14,7 +15,7 @@ service ResourceService {
|
|||||||
|
|
||||||
message Resource {
|
message Resource {
|
||||||
int32 id = 1;
|
int32 id = 1;
|
||||||
int64 created_ts = 2;
|
google.protobuf.Timestamp created_ts = 2;
|
||||||
string filename = 3;
|
string filename = 3;
|
||||||
string external_link = 4;
|
string external_link = 4;
|
||||||
string type = 5;
|
string type = 5;
|
||||||
|
@ -257,7 +257,7 @@
|
|||||||
| Field | Type | Label | Description |
|
| Field | Type | Label | Description |
|
||||||
| ----- | ---- | ----- | ----------- |
|
| ----- | ---- | ----- | ----------- |
|
||||||
| id | [int32](#int32) | | |
|
| id | [int32](#int32) | | |
|
||||||
| created_ts | [int64](#int64) | | |
|
| created_ts | [google.protobuf.Timestamp](#google-protobuf-Timestamp) | | |
|
||||||
| filename | [string](#string) | | |
|
| filename | [string](#string) | | |
|
||||||
| external_link | [string](#string) | | |
|
| external_link | [string](#string) | | |
|
||||||
| type | [string](#string) | | |
|
| type | [string](#string) | | |
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
_ "google.golang.org/genproto/googleapis/api/annotations"
|
_ "google.golang.org/genproto/googleapis/api/annotations"
|
||||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||||
|
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
|
||||||
reflect "reflect"
|
reflect "reflect"
|
||||||
sync "sync"
|
sync "sync"
|
||||||
)
|
)
|
||||||
@ -26,13 +27,13 @@ type Resource struct {
|
|||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
|
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"`
|
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"`
|
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"`
|
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"`
|
Type string `protobuf:"bytes,5,opt,name=type,proto3" json:"type,omitempty"`
|
||||||
Size int64 `protobuf:"varint,6,opt,name=size,proto3" json:"size,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"`
|
RelatedMemoId *int32 `protobuf:"varint,7,opt,name=related_memo_id,json=relatedMemoId,proto3,oneof" json:"related_memo_id,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *Resource) Reset() {
|
func (x *Resource) Reset() {
|
||||||
@ -74,11 +75,11 @@ func (x *Resource) GetId() int32 {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *Resource) GetCreatedTs() int64 {
|
func (x *Resource) GetCreatedTs() *timestamppb.Timestamp {
|
||||||
if x != nil {
|
if x != nil {
|
||||||
return x.CreatedTs
|
return x.CreatedTs
|
||||||
}
|
}
|
||||||
return 0
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *Resource) GetFilename() string {
|
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,
|
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,
|
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,
|
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,
|
0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f,
|
||||||
0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01,
|
0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d,
|
||||||
0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61,
|
0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xff, 0x01, 0x0a,
|
||||||
0x74, 0x65, 0x64, 0x5f, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x72,
|
0x08, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18,
|
||||||
0x65, 0x61, 0x74, 0x65, 0x64, 0x54, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x6e,
|
0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65,
|
||||||
0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x6e,
|
0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e,
|
||||||
0x61, 0x6d, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f,
|
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
|
||||||
0x6c, 0x69, 0x6e, 0x6b, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x78, 0x74, 0x65,
|
0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74,
|
||||||
0x72, 0x6e, 0x61, 0x6c, 0x4c, 0x69, 0x6e, 0x6b, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65,
|
0x65, 0x64, 0x54, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x6e, 0x61, 0x6d, 0x65,
|
||||||
0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04,
|
0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x6e, 0x61, 0x6d, 0x65,
|
||||||
0x73, 0x69, 0x7a, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65,
|
0x12, 0x23, 0x0a, 0x0d, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x6c, 0x69, 0x6e,
|
||||||
0x12, 0x2b, 0x0a, 0x0f, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x6d, 0x65, 0x6d, 0x6f,
|
0x6b, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61,
|
||||||
0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, 0x0d, 0x72, 0x65, 0x6c,
|
0x6c, 0x4c, 0x69, 0x6e, 0x6b, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x05, 0x20,
|
||||||
0x61, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x6d, 0x6f, 0x49, 0x64, 0x88, 0x01, 0x01, 0x42, 0x12, 0x0a,
|
0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a,
|
||||||
0x10, 0x5f, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x6d, 0x65, 0x6d, 0x6f, 0x5f, 0x69,
|
0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x2b, 0x0a,
|
||||||
0x64, 0x22, 0x16, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
|
0x0f, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x6d, 0x65, 0x6d, 0x6f, 0x5f, 0x69, 0x64,
|
||||||
0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x4d, 0x0a, 0x15, 0x4c, 0x69, 0x73,
|
0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, 0x0d, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x65,
|
||||||
0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
|
0x64, 0x4d, 0x65, 0x6d, 0x6f, 0x49, 0x64, 0x88, 0x01, 0x01, 0x42, 0x12, 0x0a, 0x10, 0x5f, 0x72,
|
||||||
0x73, 0x65, 0x12, 0x34, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18,
|
0x65, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x6d, 0x65, 0x6d, 0x6f, 0x5f, 0x69, 0x64, 0x22, 0x16,
|
||||||
0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70,
|
0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52,
|
||||||
0x69, 0x2e, 0x76, 0x32, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72,
|
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x4d, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65,
|
||||||
0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x32, 0x86, 0x01, 0x0a, 0x0f, 0x52, 0x65, 0x73,
|
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
|
||||||
0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x73, 0x0a, 0x0d,
|
0x34, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03,
|
||||||
0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x22, 0x2e,
|
0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76,
|
||||||
0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73,
|
0x32, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f,
|
||||||
0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
|
0x75, 0x72, 0x63, 0x65, 0x73, 0x32, 0x86, 0x01, 0x0a, 0x0f, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72,
|
||||||
0x74, 0x1a, 0x23, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32,
|
0x63, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x73, 0x0a, 0x0d, 0x4c, 0x69, 0x73,
|
||||||
0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65,
|
0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x22, 0x2e, 0x6d, 0x65, 0x6d,
|
||||||
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x19, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x13, 0x12, 0x11,
|
0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65,
|
||||||
0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
|
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23,
|
||||||
0x73, 0x42, 0xac, 0x01, 0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e,
|
0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69,
|
||||||
0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x42, 0x14, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
|
0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
|
||||||
0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x30,
|
0x6e, 0x73, 0x65, 0x22, 0x19, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x13, 0x12, 0x11, 0x2f, 0x61, 0x70,
|
||||||
0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x75, 0x73, 0x65, 0x6d, 0x65,
|
0x69, 0x2f, 0x76, 0x32, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x42, 0xac,
|
||||||
0x6d, 0x6f, 0x73, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f,
|
0x01, 0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69,
|
||||||
0x67, 0x65, 0x6e, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x3b, 0x61, 0x70, 0x69, 0x76, 0x32,
|
0x2e, 0x76, 0x32, 0x42, 0x14, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x72,
|
||||||
0xa2, 0x02, 0x03, 0x4d, 0x41, 0x58, 0xaa, 0x02, 0x0c, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x41,
|
0x76, 0x69, 0x63, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x30, 0x67, 0x69, 0x74,
|
||||||
0x70, 0x69, 0x2e, 0x56, 0x32, 0xca, 0x02, 0x0c, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x5c, 0x41, 0x70,
|
0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x75, 0x73, 0x65, 0x6d, 0x65, 0x6d, 0x6f, 0x73,
|
||||||
0x69, 0x5c, 0x56, 0x32, 0xe2, 0x02, 0x18, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x5c, 0x41, 0x70, 0x69,
|
0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x65, 0x6e,
|
||||||
0x5c, 0x56, 0x32, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea,
|
0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x3b, 0x61, 0x70, 0x69, 0x76, 0x32, 0xa2, 0x02, 0x03,
|
||||||
0x02, 0x0e, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x3a, 0x3a, 0x41, 0x70, 0x69, 0x3a, 0x3a, 0x56, 0x32,
|
0x4d, 0x41, 0x58, 0xaa, 0x02, 0x0c, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x41, 0x70, 0x69, 0x2e,
|
||||||
0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
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 (
|
var (
|
||||||
@ -269,16 +274,18 @@ var file_api_v2_resource_service_proto_goTypes = []interface{}{
|
|||||||
(*Resource)(nil), // 0: memos.api.v2.Resource
|
(*Resource)(nil), // 0: memos.api.v2.Resource
|
||||||
(*ListResourcesRequest)(nil), // 1: memos.api.v2.ListResourcesRequest
|
(*ListResourcesRequest)(nil), // 1: memos.api.v2.ListResourcesRequest
|
||||||
(*ListResourcesResponse)(nil), // 2: memos.api.v2.ListResourcesResponse
|
(*ListResourcesResponse)(nil), // 2: memos.api.v2.ListResourcesResponse
|
||||||
|
(*timestamppb.Timestamp)(nil), // 3: google.protobuf.Timestamp
|
||||||
}
|
}
|
||||||
var file_api_v2_resource_service_proto_depIdxs = []int32{
|
var file_api_v2_resource_service_proto_depIdxs = []int32{
|
||||||
0, // 0: memos.api.v2.ListResourcesResponse.resources:type_name -> memos.api.v2.Resource
|
3, // 0: memos.api.v2.Resource.created_ts:type_name -> google.protobuf.Timestamp
|
||||||
1, // 1: memos.api.v2.ResourceService.ListResources:input_type -> memos.api.v2.ListResourcesRequest
|
0, // 1: memos.api.v2.ListResourcesResponse.resources:type_name -> memos.api.v2.Resource
|
||||||
2, // 2: memos.api.v2.ResourceService.ListResources:output_type -> memos.api.v2.ListResourcesResponse
|
1, // 2: memos.api.v2.ResourceService.ListResources:input_type -> memos.api.v2.ListResourcesRequest
|
||||||
2, // [2:3] is the sub-list for method output_type
|
2, // 3: memos.api.v2.ResourceService.ListResources:output_type -> memos.api.v2.ListResourcesResponse
|
||||||
1, // [1:2] is the sub-list for method input_type
|
3, // [3:4] is the sub-list for method output_type
|
||||||
1, // [1:1] is the sub-list for extension type_name
|
2, // [2:3] is the sub-list for method input_type
|
||||||
1, // [1:1] is the sub-list for extension extendee
|
2, // [2:2] is the sub-list for extension type_name
|
||||||
0, // [0:1] is the sub-list for field 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() }
|
func init() { file_api_v2_resource_service_proto_init() }
|
||||||
|
@ -28,13 +28,14 @@ type Resource struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FindResource struct {
|
type FindResource struct {
|
||||||
GetBlob bool
|
GetBlob bool
|
||||||
ID *int32
|
ID *int32
|
||||||
CreatorID *int32
|
CreatorID *int32
|
||||||
Filename *string
|
Filename *string
|
||||||
MemoID *int32
|
MemoID *int32
|
||||||
Limit *int
|
HasRelatedMemo bool
|
||||||
Offset *int
|
Limit *int
|
||||||
|
Offset *int
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateResource struct {
|
type UpdateResource struct {
|
||||||
@ -96,6 +97,9 @@ func (s *Store) ListResources(ctx context.Context, find *FindResource) ([]*Resou
|
|||||||
if v := find.MemoID; v != nil {
|
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)
|
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"}
|
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 {
|
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
|
LEFT JOIN memo_resource ON resource.id = memo_resource.resource_id
|
||||||
WHERE %s
|
WHERE %s
|
||||||
GROUP BY resource.id
|
GROUP BY resource.id
|
||||||
ORDER BY resource.id DESC
|
ORDER BY resource.created_ts DESC
|
||||||
`, strings.Join(fields, ", "), strings.Join(where, " AND "))
|
`, strings.Join(fields, ", "), strings.Join(where, " AND "))
|
||||||
if find.Limit != nil {
|
if find.Limit != nil {
|
||||||
query = fmt.Sprintf("%s LIMIT %d", query, *find.Limit)
|
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 { Autocomplete, Button, Input, List, ListItem, Option, Select, Typography } from "@mui/joy";
|
||||||
import React, { useRef, useState } from "react";
|
import React, { useRef, useState } from "react";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
|
import { Resource } from "@/types/proto/api/v2/resource_service_pb";
|
||||||
import { useTranslate } from "@/utils/i18n";
|
import { useTranslate } from "@/utils/i18n";
|
||||||
import { useResourceStore } from "../store/module";
|
import { useResourceStore } from "../store/module";
|
||||||
import { generateDialog } from "./Dialog";
|
import { generateDialog } from "./Dialog";
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { Resource } from "@/types/proto/api/v2/resource_service_pb";
|
||||||
import Icon from "../Icon";
|
import Icon from "../Icon";
|
||||||
import ResourceIcon from "../ResourceIcon";
|
import ResourceIcon from "../ResourceIcon";
|
||||||
|
|
||||||
@ -23,7 +24,7 @@ const ResourceListView = (props: Props) => {
|
|||||||
key={resource.id}
|
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"
|
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>
|
<span className="text-sm max-w-[8rem] truncate">{resource.filename}</span>
|
||||||
<Icon.X
|
<Icon.X
|
||||||
className="w-4 h-auto cursor-pointer opacity-60 hover:opacity-100"
|
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 { clearContentQueryParam } from "@/helpers/utils";
|
||||||
import { getMatchedNodes } from "@/labs/marked";
|
import { getMatchedNodes } from "@/labs/marked";
|
||||||
import { useFilterStore, useGlobalStore, useMemoStore, useResourceStore, useTagStore, useUserStore } from "@/store/module";
|
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 { useTranslate } from "@/utils/i18n";
|
||||||
import showCreateResourceDialog from "../CreateResourceDialog";
|
import showCreateResourceDialog from "../CreateResourceDialog";
|
||||||
import Icon from "../Icon";
|
import Icon from "../Icon";
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
|
import { Resource } from "@/types/proto/api/v2/resource_service_pb";
|
||||||
import { getResourceUrl } from "@/utils/resource";
|
import { getResourceUrl } from "@/utils/resource";
|
||||||
import Icon from "./Icon";
|
import ResourceIcon from "./ResourceIcon";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
resource: Resource;
|
resource: Resource;
|
||||||
@ -16,7 +17,7 @@ const MemoResource: React.FC<Props> = (props: Props) => {
|
|||||||
|
|
||||||
return (
|
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") ? (
|
{resource.type.startsWith("audio") ? (
|
||||||
<>
|
<>
|
||||||
<audio controls>
|
<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" />
|
<ResourceIcon className="!w-4 !h-4 mr-1" resource={resource} />
|
||||||
<span className="text-gray-500 text-sm max-w-[256px] truncate font-mono cursor-pointer" onClick={handlePreviewBtnClick}>
|
<span className="text-sm max-w-[256px] truncate cursor-pointer" onClick={handlePreviewBtnClick}>
|
||||||
{resource.filename}
|
{resource.filename}
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { absolutifyLink } from "@/helpers/utils";
|
import { absolutifyLink } from "@/helpers/utils";
|
||||||
|
import { Resource } from "@/types/proto/api/v2/resource_service_pb";
|
||||||
import { getResourceType, getResourceUrl } from "@/utils/resource";
|
import { getResourceType, getResourceUrl } from "@/utils/resource";
|
||||||
import MemoResource from "./MemoResource";
|
import MemoResource from "./MemoResource";
|
||||||
import showPreviewImageDialog from "./PreviewImageDialog";
|
import showPreviewImageDialog from "./PreviewImageDialog";
|
||||||
@ -42,7 +43,7 @@ const MemoResourceListView: React.FC<Props> = (props: Props) => {
|
|||||||
<>
|
<>
|
||||||
{imageResourceList.length > 0 &&
|
{imageResourceList.length > 0 &&
|
||||||
(imageResourceList.length === 1 ? (
|
(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
|
<img
|
||||||
className="cursor-pointer min-h-full w-auto min-w-full object-cover"
|
className="cursor-pointer min-h-full w-auto min-w-full object-cover"
|
||||||
src={getResourceUrl(imageResourceList[0])}
|
src={getResourceUrl(imageResourceList[0])}
|
||||||
@ -63,7 +64,7 @@ const MemoResourceListView: React.FC<Props> = (props: Props) => {
|
|||||||
return (
|
return (
|
||||||
<SquareDiv
|
<SquareDiv
|
||||||
key={resource.id}
|
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
|
<img
|
||||||
className="cursor-pointer min-h-full w-auto min-w-full object-cover"
|
className="cursor-pointer min-h-full w-auto min-w-full object-cover"
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { getDateTimeString } from "@/helpers/datetime";
|
import { getDateTimeString } from "@/helpers/datetime";
|
||||||
|
import { Resource } from "@/types/proto/api/v2/resource_service_pb";
|
||||||
import ResourceIcon from "./ResourceIcon";
|
import ResourceIcon from "./ResourceIcon";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { Resource } from "@/types/proto/api/v2/resource_service_pb";
|
||||||
import { getResourceType, getResourceUrl } from "@/utils/resource";
|
import { getResourceType, getResourceUrl } from "@/utils/resource";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
import showPreviewImageDialog from "./PreviewImageDialog";
|
import showPreviewImageDialog from "./PreviewImageDialog";
|
||||||
@ -18,38 +19,52 @@ const ResourceIcon = (props: Props) => {
|
|||||||
const className = classNames("w-full h-auto", props.className);
|
const className = classNames("w-full h-auto", props.className);
|
||||||
const strokeWidth = props.strokeWidth;
|
const strokeWidth = props.strokeWidth;
|
||||||
|
|
||||||
switch (resourceType) {
|
const previewResource = () => {
|
||||||
case "image/*":
|
window.open(resourceUrl);
|
||||||
return (
|
};
|
||||||
<SquareDiv className={classNames(className, "flex items-center justify-center overflow-clip")}>
|
|
||||||
<img
|
if (resourceType === "image/*") {
|
||||||
className="max-w-full max-h-full object-cover shadow"
|
return (
|
||||||
src={resource.externalLink ? resourceUrl : resourceUrl + "?thumbnail=1"}
|
<SquareDiv className={classNames(className, "flex items-center justify-center overflow-clip")}>
|
||||||
onClick={() => showPreviewImageDialog(resourceUrl)}
|
<img
|
||||||
/>
|
className="min-w-full min-h-full object-cover shadow"
|
||||||
</SquareDiv>
|
src={resource.externalLink ? resourceUrl : resourceUrl + "?thumbnail=1"}
|
||||||
);
|
onClick={() => showPreviewImageDialog(resourceUrl)}
|
||||||
case "video/*":
|
/>
|
||||||
return <Icon.FileVideo2 strokeWidth={strokeWidth} className={classNames(className, "opacity-50")} />;
|
</SquareDiv>
|
||||||
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 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);
|
export default React.memo(ResourceIcon);
|
||||||
|
@ -64,7 +64,7 @@ const AccessTokenSection = () => {
|
|||||||
Access Tokens
|
Access Tokens
|
||||||
<LearnMore className="ml-2" url="https://usememos.com/docs/local-storage" />
|
<LearnMore className="ml-2" url="https://usememos.com/docs/local-storage" />
|
||||||
</p>
|
</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>
|
||||||
<div className="mt-4 sm:mt-0">
|
<div className="mt-4 sm:mt-0">
|
||||||
<Button
|
<Button
|
||||||
@ -81,7 +81,7 @@ const AccessTokenSection = () => {
|
|||||||
<div className="mt-2 flow-root">
|
<div className="mt-2 flow-root">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<div className="inline-block min-w-full py-2 align-middle">
|
<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>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-400">
|
<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>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-200">
|
<tbody className="divide-y divide-gray-200 dark:divide-gray-500">
|
||||||
{userAccessTokens.map((userAccessToken) => (
|
{userAccessTokens.map((userAccessToken) => (
|
||||||
<tr key={userAccessToken.accessToken}>
|
<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">
|
<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 axios from "axios";
|
||||||
|
import { Resource } from "@/types/proto/api/v2/resource_service_pb";
|
||||||
import { GetUserResponse } from "@/types/proto/api/v2/user_service_pb";
|
import { GetUserResponse } from "@/types/proto/api/v2/user_service_pb";
|
||||||
|
|
||||||
export function getSystemStatus() {
|
export function getSystemStatus() {
|
||||||
@ -143,17 +144,6 @@ export function getResourceList() {
|
|||||||
return axios.get<Resource[]>("/api/v1/resource");
|
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) {
|
export function createResource(resourceCreate: ResourceCreate) {
|
||||||
return axios.post<Resource>("/api/v1/resource", 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"
|
* - "pt-BR" locale: "30/01/2023 22:05:00"
|
||||||
* - "pl" 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());
|
const tsFromDate = getTimeStampByDate(t ? t : Date.now());
|
||||||
|
|
||||||
return new Date(tsFromDate).toLocaleDateString(locale, {
|
return new Date(tsFromDate).toLocaleDateString(locale, {
|
||||||
|
@ -1,29 +1,45 @@
|
|||||||
import { useEffect } from "react";
|
import axios from "axios";
|
||||||
import { toast } from "react-hot-toast";
|
import { useEffect, useState } from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
import Empty from "@/components/Empty";
|
import Empty from "@/components/Empty";
|
||||||
import Icon from "@/components/Icon";
|
import Icon from "@/components/Icon";
|
||||||
import MobileHeader from "@/components/MobileHeader";
|
import MobileHeader from "@/components/MobileHeader";
|
||||||
import ResourceCard from "@/components/ResourceCard";
|
import ResourceIcon from "@/components/ResourceIcon";
|
||||||
import useLoading from "@/hooks/useLoading";
|
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";
|
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 Resources = () => {
|
||||||
const t = useTranslate();
|
const t = useTranslate();
|
||||||
const loadingState = useLoading();
|
const loadingState = useLoading();
|
||||||
const resourceStore = useResourceStore();
|
const [resources, setResources] = useState<Resource[]>([]);
|
||||||
const resources = resourceStore.state.resources;
|
const groupedResources = groupResourcesByDate(resources);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
resourceStore
|
fetchAllResources().then((resources) => {
|
||||||
.fetchResourceList()
|
setResources(resources);
|
||||||
.then(() => {
|
loadingState.setFinish();
|
||||||
loadingState.setFinish();
|
});
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(error);
|
|
||||||
toast.error(error.response.data.message);
|
|
||||||
});
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -41,22 +57,51 @@ const Resources = () => {
|
|||||||
<p className="w-full text-center text-base my-6 mt-8">{t("resource.fetching-data")}</p>
|
<p className="w-full text-center text-base my-6 mt-8">{t("resource.fetching-data")}</p>
|
||||||
</div>
|
</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 ? (
|
{resources.length === 0 ? (
|
||||||
<div className="w-full mt-8 mb-8 flex flex-col justify-center items-center italic">
|
<div className="w-full mt-8 mb-8 flex flex-col justify-center items-center italic">
|
||||||
<Empty />
|
<Empty />
|
||||||
<p className="mt-4 text-gray-600 dark:text-gray-400">{t("message.no-data")}</p>
|
<p className="mt-4 text-gray-600 dark:text-gray-400">{t("message.no-data")}</p>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,17 +1,10 @@
|
|||||||
import * as api from "@/helpers/api";
|
import * as api from "@/helpers/api";
|
||||||
|
import { Resource } from "@/types/proto/api/v2/resource_service_pb";
|
||||||
import { useTranslate } from "@/utils/i18n";
|
import { useTranslate } from "@/utils/i18n";
|
||||||
import store, { useAppSelector } from "../";
|
import store, { useAppSelector } from "../";
|
||||||
import { deleteResource, patchResource, setResources } from "../reducer/resource";
|
import { deleteResource, patchResource, setResources } from "../reducer/resource";
|
||||||
import { useGlobalStore } from "./global";
|
import { useGlobalStore } from "./global";
|
||||||
|
|
||||||
const convertResponseModelResource = (resource: Resource): Resource => {
|
|
||||||
return {
|
|
||||||
...resource,
|
|
||||||
createdTs: resource.createdTs * 1000,
|
|
||||||
updatedTs: resource.updatedTs * 1000,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useResourceStore = () => {
|
export const useResourceStore = () => {
|
||||||
const state = useAppSelector((state) => state.resource);
|
const state = useAppSelector((state) => state.resource);
|
||||||
const t = useTranslate();
|
const t = useTranslate();
|
||||||
@ -24,14 +17,12 @@ export const useResourceStore = () => {
|
|||||||
return store.getState().resource;
|
return store.getState().resource;
|
||||||
},
|
},
|
||||||
async fetchResourceList(): Promise<Resource[]> {
|
async fetchResourceList(): Promise<Resource[]> {
|
||||||
const { data } = await api.getResourceList();
|
const { data: resourceList } = await api.getResourceList();
|
||||||
const resourceList = data.map((m) => convertResponseModelResource(m));
|
|
||||||
store.dispatch(setResources(resourceList));
|
store.dispatch(setResources(resourceList));
|
||||||
return resourceList;
|
return resourceList;
|
||||||
},
|
},
|
||||||
async createResource(resourceCreate: ResourceCreate): Promise<Resource> {
|
async createResource(resourceCreate: ResourceCreate): Promise<Resource> {
|
||||||
const { data } = await api.createResource(resourceCreate);
|
const { data: resource } = await api.createResource(resourceCreate);
|
||||||
const resource = convertResponseModelResource(data);
|
|
||||||
const resourceList = state.resources;
|
const resourceList = state.resources;
|
||||||
store.dispatch(setResources([resource, ...resourceList]));
|
store.dispatch(setResources([resource, ...resourceList]));
|
||||||
return resource;
|
return resource;
|
||||||
@ -44,8 +35,7 @@ export const useResourceStore = () => {
|
|||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", file, filename);
|
formData.append("file", file, filename);
|
||||||
const { data } = await api.createResourceWithBlob(formData);
|
const { data: resource } = await api.createResourceWithBlob(formData);
|
||||||
const resource = convertResponseModelResource(data);
|
|
||||||
const resourceList = state.resources;
|
const resourceList = state.resources;
|
||||||
store.dispatch(setResources([resource, ...resourceList]));
|
store.dispatch(setResources([resource, ...resourceList]));
|
||||||
return resource;
|
return resource;
|
||||||
@ -60,8 +50,7 @@ export const useResourceStore = () => {
|
|||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", file, filename);
|
formData.append("file", file, filename);
|
||||||
const { data } = await api.createResourceWithBlob(formData);
|
const { data: resource } = await api.createResourceWithBlob(formData);
|
||||||
const resource = convertResponseModelResource(data);
|
|
||||||
newResourceList = [resource, ...newResourceList];
|
newResourceList = [resource, ...newResourceList];
|
||||||
}
|
}
|
||||||
const resourceList = state.resources;
|
const resourceList = state.resources;
|
||||||
@ -73,8 +62,7 @@ export const useResourceStore = () => {
|
|||||||
store.dispatch(deleteResource(id));
|
store.dispatch(deleteResource(id));
|
||||||
},
|
},
|
||||||
async patchResource(resourcePatch: ResourcePatch): Promise<Resource> {
|
async patchResource(resourcePatch: ResourcePatch): Promise<Resource> {
|
||||||
const { data } = await api.patchResource(resourcePatch);
|
const { data: resource } = await api.patchResource(resourcePatch);
|
||||||
const resource = convertResponseModelResource(data);
|
|
||||||
store.dispatch(patchResource(resource));
|
store.dispatch(patchResource(resource));
|
||||||
return resource;
|
return resource;
|
||||||
},
|
},
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||||
import { uniqBy } from "lodash-es";
|
import { uniqBy } from "lodash-es";
|
||||||
|
import { Resource } from "@/types/proto/api/v2/resource_service_pb";
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
resources: Resource[];
|
resources: Resource[];
|
||||||
|
2
web/src/types/modules/memo.d.ts
vendored
2
web/src/types/modules/memo.d.ts
vendored
@ -16,7 +16,7 @@ interface Memo {
|
|||||||
pinned: boolean;
|
pinned: boolean;
|
||||||
|
|
||||||
creatorName: string;
|
creatorName: string;
|
||||||
resourceList: Resource[];
|
resourceList: any[];
|
||||||
relationList: MemoRelation[];
|
relationList: MemoRelation[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
12
web/src/types/modules/resource.d.ts
vendored
12
web/src/types/modules/resource.d.ts
vendored
@ -1,17 +1,5 @@
|
|||||||
type ResourceId = number;
|
type ResourceId = number;
|
||||||
|
|
||||||
interface Resource {
|
|
||||||
id: ResourceId;
|
|
||||||
|
|
||||||
createdTs: number;
|
|
||||||
updatedTs: number;
|
|
||||||
|
|
||||||
filename: string;
|
|
||||||
externalLink: string;
|
|
||||||
type: string;
|
|
||||||
size: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ResourceCreate {
|
interface ResourceCreate {
|
||||||
filename: string;
|
filename: string;
|
||||||
externalLink: string;
|
externalLink: string;
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
// @ts-nocheck
|
// @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";
|
import { Message, proto3 } from "@bufbuild/protobuf";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -16,9 +16,9 @@ export declare class Resource extends Message<Resource> {
|
|||||||
id: number;
|
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;
|
* @generated from field: string filename = 3;
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
|
|
||||||
import { proto3 } from "@bufbuild/protobuf";
|
import { proto3, Timestamp } from "@bufbuild/protobuf";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @generated from message memos.api.v2.Resource
|
* @generated from message memos.api.v2.Resource
|
||||||
@ -12,7 +12,7 @@ export const Resource = proto3.makeMessageType(
|
|||||||
"memos.api.v2.Resource",
|
"memos.api.v2.Resource",
|
||||||
() => [
|
() => [
|
||||||
{ no: 1, name: "id", kind: "scalar", T: 5 /* ScalarType.INT32 */ },
|
{ 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: 3, name: "filename", kind: "scalar", T: 9 /* ScalarType.STRING */ },
|
||||||
{ no: 4, name: "external_link", 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 */ },
|
{ no: 5, name: "type", kind: "scalar", T: 9 /* ScalarType.STRING */ },
|
||||||
|
7
web/src/types/resourceItem.d.ts
vendored
7
web/src/types/resourceItem.d.ts
vendored
@ -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) => {
|
export const getResourceUrl = (resource: Resource, withOrigin = true) => {
|
||||||
if (resource.externalLink) {
|
if (resource.externalLink) {
|
||||||
return resource.externalLink;
|
return resource.externalLink;
|
||||||
|
Reference in New Issue
Block a user