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
84564891
Commit
84564891
authored
Aug 06, 2022
by
boojack
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: add view resource dialog
parent
8c8bb9e5
Changes
15
Hide whitespace changes
Inline
Side-by-side
Showing
15 changed files
with
279 additions
and
18 deletions
+279
-18
resource.go
api/resource.go
+3
-0
resource.go
server/resource.go
+7
-1
resource.go
store/resource.go
+6
-3
ArchivedMemoDialog.tsx
web/src/components/ArchivedMemoDialog.tsx
+1
-1
ResourcesDialog.tsx
web/src/components/ResourcesDialog.tsx
+108
-0
Sidebar.tsx
web/src/components/Sidebar.tsx
+8
-0
Dropdown.tsx
web/src/components/common/Dropdown.tsx
+40
-0
api.ts
web/src/helpers/api.ts
+8
-0
archived-memo-dialog.less
web/src/less/archived-memo-dialog.less
+3
-6
common-dialog.less
web/src/less/common-dialog.less
+1
-5
dropdown.less
web/src/less/common/dropdown.less
+21
-0
resources-dialog.less
web/src/less/resources-dialog.less
+55
-0
shortcut-list.less
web/src/less/shortcut-list.less
+1
-1
resourceService.ts
web/src/services/resourceService.ts
+16
-0
resource.d.ts
web/src/types/modules/resource.d.ts
+1
-1
No files found.
api/resource.go
View file @
84564891
...
@@ -38,4 +38,7 @@ type ResourceFind struct {
...
@@ -38,4 +38,7 @@ type ResourceFind struct {
type
ResourceDelete
struct
{
type
ResourceDelete
struct
{
ID
int
ID
int
// Standard fields
CreatorID
int
}
}
server/resource.go
View file @
84564891
...
@@ -138,13 +138,19 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
...
@@ -138,13 +138,19 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
})
})
g
.
DELETE
(
"/resource/:resourceId"
,
func
(
c
echo
.
Context
)
error
{
g
.
DELETE
(
"/resource/:resourceId"
,
func
(
c
echo
.
Context
)
error
{
userID
,
ok
:=
c
.
Get
(
getUserIDContextKey
())
.
(
int
)
if
!
ok
{
return
echo
.
NewHTTPError
(
http
.
StatusUnauthorized
,
"Missing user in session"
)
}
resourceID
,
err
:=
strconv
.
Atoi
(
c
.
Param
(
"resourceId"
))
resourceID
,
err
:=
strconv
.
Atoi
(
c
.
Param
(
"resourceId"
))
if
err
!=
nil
{
if
err
!=
nil
{
return
echo
.
NewHTTPError
(
http
.
StatusBadRequest
,
fmt
.
Sprintf
(
"ID is not a number: %s"
,
c
.
Param
(
"resourceId"
)))
.
SetInternal
(
err
)
return
echo
.
NewHTTPError
(
http
.
StatusBadRequest
,
fmt
.
Sprintf
(
"ID is not a number: %s"
,
c
.
Param
(
"resourceId"
)))
.
SetInternal
(
err
)
}
}
resourceDelete
:=
&
api
.
ResourceDelete
{
resourceDelete
:=
&
api
.
ResourceDelete
{
ID
:
resourceID
,
ID
:
resourceID
,
CreatorID
:
userID
,
}
}
if
err
:=
s
.
Store
.
DeleteResource
(
resourceDelete
);
err
!=
nil
{
if
err
:=
s
.
Store
.
DeleteResource
(
resourceDelete
);
err
!=
nil
{
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
"Failed to delete resource"
)
.
SetInternal
(
err
)
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
"Failed to delete resource"
)
.
SetInternal
(
err
)
...
...
store/resource.go
View file @
84564891
...
@@ -102,7 +102,7 @@ func createResource(db *sql.DB, create *api.ResourceCreate) (*resourceRaw, error
...
@@ -102,7 +102,7 @@ func createResource(db *sql.DB, create *api.ResourceCreate) (*resourceRaw, error
creator_id
creator_id
)
)
VALUES (?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?)
RETURNING id, filename, blob, type, size, created_ts, updated_ts
RETURNING id, filename, blob, type, size, creat
or_id, creat
ed_ts, updated_ts
`
,
`
,
create
.
Filename
,
create
.
Filename
,
create
.
Blob
,
create
.
Blob
,
...
@@ -123,6 +123,7 @@ func createResource(db *sql.DB, create *api.ResourceCreate) (*resourceRaw, error
...
@@ -123,6 +123,7 @@ func createResource(db *sql.DB, create *api.ResourceCreate) (*resourceRaw, error
&
resourceRaw
.
Blob
,
&
resourceRaw
.
Blob
,
&
resourceRaw
.
Type
,
&
resourceRaw
.
Type
,
&
resourceRaw
.
Size
,
&
resourceRaw
.
Size
,
&
resourceRaw
.
CreatorID
,
&
resourceRaw
.
CreatedTs
,
&
resourceRaw
.
CreatedTs
,
&
resourceRaw
.
UpdatedTs
,
&
resourceRaw
.
UpdatedTs
,
);
err
!=
nil
{
);
err
!=
nil
{
...
@@ -152,6 +153,7 @@ func findResourceList(db *sql.DB, find *api.ResourceFind) ([]*resourceRaw, error
...
@@ -152,6 +153,7 @@ func findResourceList(db *sql.DB, find *api.ResourceFind) ([]*resourceRaw, error
blob,
blob,
type,
type,
size,
size,
creator_id,
created_ts,
created_ts,
updated_ts
updated_ts
FROM resource
FROM resource
...
@@ -173,6 +175,7 @@ func findResourceList(db *sql.DB, find *api.ResourceFind) ([]*resourceRaw, error
...
@@ -173,6 +175,7 @@ func findResourceList(db *sql.DB, find *api.ResourceFind) ([]*resourceRaw, error
&
resourceRaw
.
Blob
,
&
resourceRaw
.
Blob
,
&
resourceRaw
.
Type
,
&
resourceRaw
.
Type
,
&
resourceRaw
.
Size
,
&
resourceRaw
.
Size
,
&
resourceRaw
.
CreatorID
,
&
resourceRaw
.
CreatedTs
,
&
resourceRaw
.
CreatedTs
,
&
resourceRaw
.
UpdatedTs
,
&
resourceRaw
.
UpdatedTs
,
);
err
!=
nil
{
);
err
!=
nil
{
...
@@ -192,8 +195,8 @@ func findResourceList(db *sql.DB, find *api.ResourceFind) ([]*resourceRaw, error
...
@@ -192,8 +195,8 @@ func findResourceList(db *sql.DB, find *api.ResourceFind) ([]*resourceRaw, error
func
deleteResource
(
db
*
sql
.
DB
,
delete
*
api
.
ResourceDelete
)
error
{
func
deleteResource
(
db
*
sql
.
DB
,
delete
*
api
.
ResourceDelete
)
error
{
result
,
err
:=
db
.
Exec
(
`
result
,
err
:=
db
.
Exec
(
`
PRAGMA foreign_keys = ON;
PRAGMA foreign_keys = ON;
DELETE FROM resource WHERE id = ?
DELETE FROM resource WHERE id = ?
AND creator_id = ?
`
,
delete
.
ID
)
`
,
delete
.
ID
,
delete
.
CreatorID
)
if
err
!=
nil
{
if
err
!=
nil
{
return
FormatError
(
err
)
return
FormatError
(
err
)
}
}
...
...
web/src/components/ArchivedMemoDialog.tsx
View file @
84564891
...
@@ -62,7 +62,7 @@ const ArchivedMemoDialog: React.FC<Props> = (props: Props) => {
...
@@ -62,7 +62,7 @@ const ArchivedMemoDialog: React.FC<Props> = (props: Props) => {
);
);
};
};
export
default
function
showArchivedMemo
():
void
{
export
default
function
showArchivedMemo
Dialog
():
void
{
generateDialog
(
generateDialog
(
{
{
className
:
"archived-memo-dialog"
,
className
:
"archived-memo-dialog"
,
...
...
web/src/components/ResourcesDialog.tsx
0 → 100644
View file @
84564891
import
{
useEffect
,
useState
}
from
"react"
;
import
*
as
utils
from
"../helpers/utils"
;
import
useLoading
from
"../hooks/useLoading"
;
import
{
resourceService
}
from
"../services"
;
import
Dropdown
from
"./common/Dropdown"
;
import
{
generateDialog
}
from
"./Dialog"
;
import
{
showCommonDialog
}
from
"./Dialog/CommonDialog"
;
import
toastHelper
from
"./Toast"
;
import
Icon
from
"./Icon"
;
import
"../less/resources-dialog.less"
;
interface
Props
extends
DialogProps
{}
const
ResourcesDialog
:
React
.
FC
<
Props
>
=
(
props
:
Props
)
=>
{
const
{
destroy
}
=
props
;
const
loadingState
=
useLoading
();
const
[
resources
,
setResources
]
=
useState
<
Resource
[]
>
([]);
useEffect
(()
=>
{
fetchResources
()
.
catch
((
error
)
=>
{
toastHelper
.
error
(
"Failed to fetch archived memos: "
,
error
);
})
.
finally
(()
=>
{
loadingState
.
setFinish
();
});
},
[]);
const
fetchResources
=
async
()
=>
{
const
data
=
await
resourceService
.
getResourceList
();
setResources
(
data
);
};
const
handleCopyResourceLinkBtnClick
=
(
resource
:
Resource
)
=>
{
utils
.
copyTextToClipboard
(
`
${
window
.
location
.
origin
}
/h/r/
${
resource
.
id
}
/
${
resource
.
filename
}
`
);
toastHelper
.
success
(
"Succeed to copy resource link to clipboard"
);
};
const
handleDeleteResourceBtnClick
=
(
resource
:
Resource
)
=>
{
showCommonDialog
({
title
:
`Delete Resource`
,
content
:
`Are you sure to delete this resource? THIS ACTION IS IRREVERSIABLE.❗️`
,
style
:
"warning"
,
onConfirm
:
async
()
=>
{
await
resourceService
.
deleteResourceById
(
resource
.
id
);
await
fetchResources
();
},
});
};
return
(
<>
<
div
className=
"dialog-header-container"
>
<
p
className=
"title-text"
>
<
span
className=
"icon-text"
>
🌄
</
span
>
Resources
</
p
>
<
button
className=
"btn close-btn"
onClick=
{
destroy
}
>
<
Icon
.
X
className=
"icon-img"
/>
</
button
>
</
div
>
<
div
className=
"dialog-content-container"
>
<
div
className=
"tip-text-container"
>
(👨💻WIP) View your static resources in memos. e.g. images
</
div
>
<
div
className=
"actions-container"
></
div
>
{
loadingState
.
isLoading
?
(
<
div
className=
"loading-text-container"
>
<
p
className=
"tip-text"
>
fetching data...
</
p
>
</
div
>
)
:
(
<
div
className=
"resource-table-container"
>
<
div
className=
"fields-container"
>
<
span
className=
"field-text"
>
ID
</
span
>
<
span
className=
"field-text name-text"
>
NAME
</
span
>
<
span
className=
"field-text"
>
TYPE
</
span
>
<
span
></
span
>
</
div
>
{
resources
.
map
((
resource
)
=>
(
<
div
key=
{
resource
.
id
}
className=
"resource-container"
>
<
span
className=
"field-text"
>
{
resource
.
id
}
</
span
>
<
span
className=
"field-text name-text"
>
{
resource
.
filename
}
</
span
>
<
span
className=
"field-text"
>
{
resource
.
type
}
</
span
>
<
div
className=
"buttons-container"
>
<
Dropdown
className=
"actions-dropdown"
>
<
button
onClick=
{
()
=>
handleCopyResourceLinkBtnClick
(
resource
)
}
>
Copy Link
</
button
>
<
button
className=
"delete-btn"
onClick=
{
()
=>
handleDeleteResourceBtnClick
(
resource
)
}
>
Delete
</
button
>
</
Dropdown
>
</
div
>
</
div
>
))
}
</
div
>
)
}
</
div
>
</>
);
};
export
default
function
showResourcesDialog
()
{
generateDialog
(
{
className
:
"resources-dialog"
,
useAppContext
:
true
,
},
ResourcesDialog
,
{}
);
}
web/src/components/Sidebar.tsx
View file @
84564891
...
@@ -4,6 +4,7 @@ import Only from "./common/OnlyWhen";
...
@@ -4,6 +4,7 @@ import Only from "./common/OnlyWhen";
import
showDailyReviewDialog
from
"./DailyReviewDialog"
;
import
showDailyReviewDialog
from
"./DailyReviewDialog"
;
import
showSettingDialog
from
"./SettingDialog"
;
import
showSettingDialog
from
"./SettingDialog"
;
import
showArchivedMemoDialog
from
"./ArchivedMemoDialog"
;
import
showArchivedMemoDialog
from
"./ArchivedMemoDialog"
;
import
showResourcesDialog
from
"./ResourcesDialog"
;
import
UserBanner
from
"./UserBanner"
;
import
UserBanner
from
"./UserBanner"
;
import
UsageHeatMap
from
"./UsageHeatMap"
;
import
UsageHeatMap
from
"./UsageHeatMap"
;
import
ShortcutList
from
"./ShortcutList"
;
import
ShortcutList
from
"./ShortcutList"
;
...
@@ -17,6 +18,10 @@ const Sidebar: React.FC<Props> = () => {
...
@@ -17,6 +18,10 @@ const Sidebar: React.FC<Props> = () => {
showSettingDialog
();
showSettingDialog
();
};
};
const
handleResourcesBtnClick
=
()
=>
{
showResourcesDialog
();
};
const
handleArchivedBtnClick
=
()
=>
{
const
handleArchivedBtnClick
=
()
=>
{
showArchivedMemoDialog
();
showArchivedMemoDialog
();
};
};
...
@@ -35,6 +40,9 @@ const Sidebar: React.FC<Props> = () => {
...
@@ -35,6 +40,9 @@ const Sidebar: React.FC<Props> = () => {
<
span
className=
"icon"
>
📅
</
span
>
Daily Review
<
span
className=
"icon"
>
📅
</
span
>
Daily Review
</
button
>
</
button
>
<
Only
when=
{
!
userService
.
isVisitorMode
()
}
>
<
Only
when=
{
!
userService
.
isVisitorMode
()
}
>
<
button
className=
"btn action-btn"
onClick=
{
handleResourcesBtnClick
}
>
<
span
className=
"icon"
>
🌄
</
span
>
Resources
</
button
>
<
button
className=
"btn action-btn"
onClick=
{
handleMyAccountBtnClick
}
>
<
button
className=
"btn action-btn"
onClick=
{
handleMyAccountBtnClick
}
>
<
span
className=
"icon"
>
⚙️
</
span
>
Setting
<
span
className=
"icon"
>
⚙️
</
span
>
Setting
</
button
>
</
button
>
...
...
web/src/components/common/Dropdown.tsx
0 → 100644
View file @
84564891
import
{
ReactNode
,
useEffect
,
useRef
}
from
"react"
;
import
useToggle
from
"../../hooks/useToggle"
;
import
Icon
from
"../Icon"
;
import
"../../less/common/dropdown.less"
;
interface
DropdownProps
{
children
?:
ReactNode
;
className
?:
string
;
}
const
Dropdown
:
React
.
FC
<
DropdownProps
>
=
(
props
:
DropdownProps
)
=>
{
const
{
children
,
className
}
=
props
;
const
[
dropdownStatus
,
toggleDropdownStatus
]
=
useToggle
(
false
);
const
dropdownWrapperRef
=
useRef
<
HTMLDivElement
>
(
null
);
useEffect
(()
=>
{
if
(
dropdownStatus
)
{
const
handleClickOutside
=
(
event
:
MouseEvent
)
=>
{
if
(
!
dropdownWrapperRef
.
current
?.
contains
(
event
.
target
as
Node
))
{
toggleDropdownStatus
(
false
);
}
};
window
.
addEventListener
(
"click"
,
handleClickOutside
,
{
capture
:
true
,
once
:
true
,
});
}
},
[
dropdownStatus
]);
return
(
<
div
ref=
{
dropdownWrapperRef
}
className=
{
`dropdown-wrapper ${className ?? ""}`
}
onClick=
{
()
=>
toggleDropdownStatus
()
}
>
<
span
className=
"trigger-button"
>
<
Icon
.
MoreHorizontal
className=
"icon-img"
/>
</
span
>
<
div
className=
{
`action-buttons-container ${dropdownStatus ? "" : "!hidden"}`
}
>
{
children
}
</
div
>
</
div
>
);
};
export
default
Dropdown
;
web/src/helpers/api.ts
View file @
84564891
...
@@ -109,10 +109,18 @@ export function deleteShortcutById(shortcutId: ShortcutId) {
...
@@ -109,10 +109,18 @@ export function deleteShortcutById(shortcutId: ShortcutId) {
return
axios
.
delete
(
`/api/shortcut/
${
shortcutId
}
`
);
return
axios
.
delete
(
`/api/shortcut/
${
shortcutId
}
`
);
}
}
export
function
getResourceList
()
{
return
axios
.
get
<
ResponseObject
<
Resource
[]
>>
(
"/api/resource"
);
}
export
function
uploadFile
(
formData
:
FormData
)
{
export
function
uploadFile
(
formData
:
FormData
)
{
return
axios
.
post
<
ResponseObject
<
Resource
>>
(
"/api/resource"
,
formData
);
return
axios
.
post
<
ResponseObject
<
Resource
>>
(
"/api/resource"
,
formData
);
}
}
export
function
deleteResourceById
(
id
:
ResourceId
)
{
return
axios
.
delete
(
`/api/resource/
${
id
}
`
);
}
export
function
getTagList
(
tagFind
?:
TagFind
)
{
export
function
getTagList
(
tagFind
?:
TagFind
)
{
const
queryList
=
[];
const
queryList
=
[];
if
(
tagFind
?.
creatorId
)
{
if
(
tagFind
?.
creatorId
)
{
...
...
web/src/less/archived-memo-dialog.less
View file @
84564891
...
@@ -7,17 +7,14 @@
...
@@ -7,17 +7,14 @@
@apply w-128 max-w-full mb-8;
@apply w-128 max-w-full mb-8;
> .dialog-content-container {
> .dialog-content-container {
.flex(column, flex-start, flex-start);
@apply w-full flex flex-col justify-start items-start;
@apply w-full overflow-y-auto;
> .tip-text-container {
> .tip-text-container {
@apply w-full h-32;
@apply w-full h-32 flex flex-col justify-center items-center;
.flex(column, center, center);
}
}
> .archived-memos-container {
> .archived-memos-container {
.flex(column, flex-start, flex-start);
@apply w-full flex flex-col justify-start items-start;
@apply w-full;
}
}
}
}
}
}
...
...
web/src/less/common-dialog.less
View file @
84564891
...
@@ -7,12 +7,8 @@
...
@@ -7,12 +7,8 @@
> .dialog-content-container {
> .dialog-content-container {
@apply flex flex-col justify-start items-start;
@apply flex flex-col justify-start items-start;
> .content-text {
@apply pt-2;
}
> .btns-container {
> .btns-container {
@apply flex flex-row justify-end items-center w-full mt-
3
;
@apply flex flex-row justify-end items-center w-full mt-
4
;
> .btn {
> .btn {
@apply text-sm py-1 px-3 mr-2 rounded-md hover:opacity-80;
@apply text-sm py-1 px-3 mr-2 rounded-md hover:opacity-80;
...
...
web/src/less/common/dropdown.less
0 → 100644
View file @
84564891
@import "../mixin.less";
.dropdown-wrapper {
@apply relative flex flex-col justify-start items-start select-none;
> .trigger-button {
@apply flex flex-row justify-center items-center border p-1 rounded shadow text-gray-600 cursor-pointer hover:opacity-80;
> .icon-img {
@apply w-4 h-auto;
}
}
> .action-buttons-container {
@apply w-28 mt-1 absolute top-full right-0 flex flex-col justify-start items-start bg-white z-1 border p-1 rounded shadow;
> button {
@apply w-full text-left px-2 text-sm leading-7 rounded hover:bg-gray-100;
}
}
}
web/src/less/resources-dialog.less
0 → 100644
View file @
84564891
@import "./mixin.less";
.resources-dialog {
@apply px-4;
> .dialog-container {
@apply w-128 max-w-full mb-8;
> .dialog-content-container {
@apply flex flex-col justify-start items-start w-full;
> .tip-text-container {
@apply w-full flex flex-row justify-start items-start border border-yellow-600 rounded px-2 py-1 mb-2 text-yellow-600 bg-yellow-50 text-sm;
}
> .loading-text-container {
@apply flex flex-col justify-center items-center w-full h-32;
}
> .resource-table-container {
@apply flex flex-col justify-start items-start w-full;
> .fields-container {
@apply px-2 py-2 w-full grid grid-cols-5 border-b;
> .field-text {
@apply font-mono text-gray-400;
}
}
> .resource-container {
@apply px-2 py-2 w-full grid grid-cols-5;
> .buttons-container {
@apply w-full flex flex-row justify-end items-center;
> .actions-dropdown {
.delete-btn {
@apply text-red-600;
}
}
}
}
.field-text {
@apply w-full truncate text-base pr-2 last:pr-0;
&.name-text {
@apply col-span-2;
}
}
}
}
}
}
web/src/less/shortcut-list.less
View file @
84564891
...
@@ -57,7 +57,7 @@
...
@@ -57,7 +57,7 @@
@apply flex flex-row justify-center items-center w-6 h-6 shrink-0;
@apply flex flex-row justify-center items-center w-6 h-6 shrink-0;
&.toggle-btn {
&.toggle-btn {
@apply
opacity-6
0;
@apply
w-4 h-auto text-gray-60
0;
&:hover {
&:hover {
& + .action-btns-wrapper {
& + .action-btns-wrapper {
...
...
web/src/services/resourceService.ts
View file @
84564891
import
*
as
api
from
"../helpers/api"
;
import
*
as
api
from
"../helpers/api"
;
const
convertResponseModelResource
=
(
resource
:
Resource
):
Resource
=>
{
return
{
...
resource
,
createdTs
:
resource
.
createdTs
*
1000
,
updatedTs
:
resource
.
updatedTs
*
1000
,
};
};
const
resourceService
=
{
const
resourceService
=
{
async
getResourceList
():
Promise
<
Resource
[]
>
{
const
{
data
}
=
(
await
api
.
getResourceList
()).
data
;
const
resourceList
=
data
.
map
((
m
)
=>
convertResponseModelResource
(
m
));
return
resourceList
;
},
/**
/**
* Upload resource file to server,
* Upload resource file to server,
* @param file file
* @param file file
...
@@ -19,6 +32,9 @@ const resourceService = {
...
@@ -19,6 +32,9 @@ const resourceService = {
return
data
;
return
data
;
},
},
async
deleteResourceById
(
id
:
ResourceId
)
{
return
api
.
deleteResourceById
(
id
);
},
};
};
export
default
resourceService
;
export
default
resourceService
;
web/src/types/modules/resource.d.ts
View file @
84564891
type
ResourceId
=
number
;
type
ResourceId
=
number
;
interface
Resource
{
interface
Resource
{
id
:
string
;
id
:
ResourceId
;
createdTs
:
TimeStamp
;
createdTs
:
TimeStamp
;
updatedTs
:
TimeStamp
;
updatedTs
:
TimeStamp
;
...
...
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