Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
canifa_note
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Vũ Hoàng Anh
canifa_note
Commits
fb1490c1
Commit
fb1490c1
authored
Sep 16, 2023
by
Steven
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: impl resources list page
parent
4424c8a2
Changes
24
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
24 changed files
with
234 additions
and
191 deletions
+234
-191
resource_service.go
api/v2/resource_service.go
+5
-2
resource_service.proto
proto/api/v2/resource_service.proto
+2
-1
README.md
proto/gen/api/v2/README.md
+1
-1
resource_service.pb.go
proto/gen/api/v2/resource_service.pb.go
+66
-59
resource.go
store/resource.go
+12
-8
CreateResourceDialog.tsx
web/src/components/CreateResourceDialog.tsx
+1
-0
ResourceListView.tsx
web/src/components/MemoEditor/ResourceListView.tsx
+2
-1
index.tsx
web/src/components/MemoEditor/index.tsx
+1
-0
MemoResource.tsx
web/src/components/MemoResource.tsx
+5
-4
MemoResourceListView.tsx
web/src/components/MemoResourceListView.tsx
+3
-2
ResourceCard.tsx
web/src/components/ResourceCard.tsx
+1
-0
ResourceIcon.tsx
web/src/components/ResourceIcon.tsx
+46
-31
AccessTokenSection.tsx
web/src/components/Settings/AccessTokenSection.tsx
+3
-3
api.ts
web/src/helpers/api.ts
+1
-11
datetime.ts
web/src/helpers/datetime.ts
+1
-1
Resources.tsx
web/src/pages/Resources.tsx
+69
-24
resource.ts
web/src/store/module/resource.ts
+6
-18
resource.ts
web/src/store/reducer/resource.ts
+1
-0
memo.d.ts
web/src/types/modules/memo.d.ts
+1
-1
resource.d.ts
web/src/types/modules/resource.d.ts
+0
-12
resource_service_pb.d.ts
web/src/types/proto/api/v2/resource_service_pb.d.ts
+3
-3
resource_service_pb.js
web/src/types/proto/api/v2/resource_service_pb.js
+2
-2
resourceItem.d.ts
web/src/types/resourceItem.d.ts
+0
-7
resource.ts
web/src/utils/resource.ts
+2
-0
No files found.
api/v2/resource_service.go
View file @
fb1490c1
...
...
@@ -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
,
...
...
proto/api/v2/resource_service.proto
View file @
fb1490c1
...
...
@@ -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
;
...
...
proto/gen/api/v2/README.md
View file @
fb1490c1
...
...
@@ -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
)
| | |
...
...
proto/gen/api/v2/resource_service.pb.go
View file @
fb1490c1
This diff is collapsed.
Click to expand it.
store/resource.go
View file @
fb1490c1
...
...
@@ -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
)
...
...
web/src/components/CreateResourceDialog.tsx
View file @
fb1490c1
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"
;
...
...
web/src/components/MemoEditor/ResourceListView.tsx
View file @
fb1490c1
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"
...
...
web/src/components/MemoEditor/index.tsx
View file @
fb1490c1
...
...
@@ -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"
;
...
...
web/src/components/MemoResource.tsx
View file @
fb1490c1
import
{
Resource
}
from
"@/types/proto/api/v2/resource_service_pb"
;
import
{
getResourceUrl
}
from
"@/utils/resource"
;
import
Icon
from
"./
Icon"
;
import
ResourceIcon
from
"./Resource
Icon"
;
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
>
</>
...
...
web/src/components/MemoResourceListView.tsx
View file @
fb1490c1
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"
...
...
web/src/components/ResourceCard.tsx
View file @
fb1490c1
import
{
getDateTimeString
}
from
"@/helpers/datetime"
;
import
{
Resource
}
from
"@/types/proto/api/v2/resource_service_pb"
;
import
ResourceIcon
from
"./ResourceIcon"
;
interface
Props
{
...
...
web/src/components/ResourceIcon.tsx
View file @
fb1490c1
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
);
web/src/components/Settings/AccessTokenSection.tsx
View file @
fb1490c1
...
...
@@ -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"
>
...
...
web/src/helpers/api.ts
View file @
fb1490c1
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
);
}
...
...
web/src/helpers/datetime.ts
View file @
fb1490c1
...
...
@@ -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
,
{
...
...
web/src/pages/Resources.tsx
View file @
fb1490c1
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
Resource
Card
from
"@/components/ResourceCard
"
;
import
Resource
Icon
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
>
...
...
web/src/store/module/resource.ts
View file @
fb1490c1
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
;
},
...
...
web/src/store/reducer/resource.ts
View file @
fb1490c1
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
[];
...
...
web/src/types/modules/memo.d.ts
View file @
fb1490c1
...
...
@@ -16,7 +16,7 @@ interface Memo {
pinned
:
boolean
;
creatorName
:
string
;
resourceList
:
Resource
[];
resourceList
:
any
[];
relationList
:
MemoRelation
[];
}
...
...
web/src/types/modules/resource.d.ts
View file @
fb1490c1
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
;
...
...
web/src/types/proto/api/v2/resource_service_pb.d.ts
View file @
fb1490c1
...
...
@@ -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;
...
...
web/src/types/proto/api/v2/resource_service_pb.js
View file @
fb1490c1
...
...
@@ -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 */
},
...
...
web/src/types/resourceItem.d.ts
deleted
100644 → 0
View file @
4424c8a2
interface
ResourceProps
{
resource
:
Resource
;
handleCheckClick
:
()
=>
void
;
handleUncheckClick
:
()
=>
void
;
}
type
ResourceItemType
=
ResourceProps
;
web/src/utils/resource.ts
View file @
fb1490c1
import
{
Resource
}
from
"@/types/proto/api/v2/resource_service_pb"
;
export
const
getResourceUrl
=
(
resource
:
Resource
,
withOrigin
=
true
)
=>
{
if
(
resource
.
externalLink
)
{
return
resource
.
externalLink
;
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment