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
dd5a23e3
Unverified
Commit
dd5a23e3
authored
Jan 22, 2023
by
boojack
Committed by
GitHub
Jan 22, 2023
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: support creating resource with external link (#988)
parent
848ecd99
Changes
5
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
253 additions
and
58 deletions
+253
-58
resource.go
api/resource.go
+1
-1
CreateResourceDialog.tsx
web/src/components/CreateResourceDialog.tsx
+243
-0
MemoEditor.tsx
web/src/components/MemoEditor.tsx
+6
-28
ResourcesDialog.tsx
web/src/components/ResourcesDialog.tsx
+2
-29
resource.d.ts
web/src/types/modules/resource.d.ts
+1
-0
No files found.
api/resource.go
View file @
dd5a23e3
...
...
@@ -27,7 +27,7 @@ type ResourceCreate struct {
Filename
string
`json:"filename"`
Blob
[]
byte
`json:"-"`
ExternalLink
string
`json:"externalLink"`
Type
string
`json:"
-
"`
Type
string
`json:"
type
"`
Size
int64
`json:"-"`
}
...
...
web/src/components/CreateResourceDialog.tsx
0 → 100644
View file @
dd5a23e3
import
{
Button
,
Input
,
Select
,
Option
,
Typography
,
List
,
ListItem
,
Autocomplete
}
from
"@mui/joy"
;
import
React
,
{
useRef
,
useState
}
from
"react"
;
import
{
useResourceStore
}
from
"../store/module"
;
import
Icon
from
"./Icon"
;
import
toastHelper
from
"./Toast"
;
import
{
generateDialog
}
from
"./Dialog"
;
const
fileTypeAutocompleteOptions
=
[
"image/*"
,
"text/*"
,
"audio/*"
,
"video/*"
,
"application/*"
];
interface
Props
extends
DialogProps
{
onCancel
?:
()
=>
void
;
onConfirm
?:
(
resourceList
:
Resource
[])
=>
void
;
}
type
SelectedMode
=
"local-file"
|
"external-link"
;
interface
State
{
selectedMode
:
SelectedMode
;
uploadingFlag
:
boolean
;
}
const
CreateResourceDialog
:
React
.
FC
<
Props
>
=
(
props
:
Props
)
=>
{
const
{
destroy
,
onCancel
,
onConfirm
}
=
props
;
const
resourceStore
=
useResourceStore
();
const
[
state
,
setState
]
=
useState
<
State
>
({
selectedMode
:
"local-file"
,
uploadingFlag
:
false
,
});
const
[
resourceCreate
,
setResourceCreate
]
=
useState
<
ResourceCreate
>
({
filename
:
""
,
externalLink
:
""
,
type
:
""
,
});
const
[
fileList
,
setFileList
]
=
useState
<
File
[]
>
([]);
const
fileInputRef
=
useRef
<
HTMLInputElement
>
(
null
);
const
handleCloseDialog
=
()
=>
{
if
(
onCancel
)
{
onCancel
();
}
destroy
();
};
const
handleSelectedModeChanged
=
(
mode
:
"local-file"
|
"external-link"
)
=>
{
setState
((
state
)
=>
{
return
{
...
state
,
selectedMode
:
mode
,
};
});
};
const
handleExternalLinkChanged
=
(
event
:
React
.
ChangeEvent
<
HTMLInputElement
>
)
=>
{
const
externalLink
=
event
.
target
.
value
;
setResourceCreate
((
state
)
=>
{
return
{
...
state
,
externalLink
,
};
});
};
const
handleFileNameChanged
=
(
event
:
React
.
ChangeEvent
<
HTMLInputElement
>
)
=>
{
const
filename
=
event
.
target
.
value
;
setResourceCreate
((
state
)
=>
{
return
{
...
state
,
filename
,
};
});
};
const
handleFileTypeChanged
=
(
fileType
:
string
)
=>
{
setResourceCreate
((
state
)
=>
{
return
{
...
state
,
type
:
fileType
,
};
});
};
const
handleFileInputChange
=
async
()
=>
{
if
(
!
fileInputRef
.
current
||
!
fileInputRef
.
current
.
files
)
{
return
;
}
const
files
:
File
[]
=
[];
for
(
const
file
of
fileInputRef
.
current
.
files
)
{
files
.
push
(
file
);
}
setFileList
(
files
);
};
const
allowConfirmAction
=
()
=>
{
if
(
state
.
selectedMode
===
"local-file"
)
{
if
(
!
fileInputRef
.
current
||
!
fileInputRef
.
current
.
files
||
fileInputRef
.
current
.
files
.
length
===
0
)
{
return
false
;
}
}
else
if
(
state
.
selectedMode
===
"external-link"
)
{
if
(
resourceCreate
.
filename
===
""
||
resourceCreate
.
externalLink
===
""
||
resourceCreate
.
type
===
""
)
{
return
false
;
}
}
return
true
;
};
const
handleConfirmBtnClick
=
async
()
=>
{
if
(
state
.
uploadingFlag
)
{
return
;
}
setState
((
state
)
=>
{
return
{
...
state
,
uploadingFlag
:
true
,
};
});
const
createdResourceList
:
Resource
[]
=
[];
try
{
if
(
state
.
selectedMode
===
"local-file"
)
{
if
(
!
fileInputRef
.
current
||
!
fileInputRef
.
current
.
files
)
{
return
;
}
for
(
const
file
of
fileInputRef
.
current
.
files
)
{
const
resource
=
await
resourceStore
.
createResourceWithBlob
(
file
);
createdResourceList
.
push
(
resource
);
}
}
else
{
const
resource
=
await
resourceStore
.
createResource
(
resourceCreate
);
createdResourceList
.
push
(
resource
);
}
}
catch
(
error
:
any
)
{
console
.
error
(
error
);
toastHelper
.
error
(
error
.
response
.
data
.
message
);
}
if
(
onConfirm
)
{
onConfirm
(
createdResourceList
);
}
destroy
();
};
return
(
<>
<
div
className=
"dialog-header-container"
>
<
p
className=
"title-text"
>
Create Resource
</
p
>
<
button
className=
"btn close-btn"
onClick=
{
handleCloseDialog
}
>
<
Icon
.
X
/>
</
button
>
</
div
>
<
div
className=
"dialog-content-container !w-80"
>
<
Typography
className=
"!mb-1"
level=
"body2"
>
Upload method
</
Typography
>
<
Select
className=
"w-full mb-2"
onChange=
{
(
_
,
value
)
=>
handleSelectedModeChanged
(
value
as
SelectedMode
)
}
value=
{
state
.
selectedMode
}
startDecorator=
{
<
Icon
.
File
className=
"w-4 h-auto"
/>
}
>
<
Option
value=
"local-file"
>
Local file
</
Option
>
<
Option
value=
"external-link"
>
External link
</
Option
>
</
Select
>
{
state
.
selectedMode
===
"local-file"
&&
(
<>
<
div
className=
"w-full relative bg-blue-50 rounded-md flex flex-row justify-center items-center py-8"
>
<
label
htmlFor=
"files"
className=
"p-2 px-4 text-sm text-white cursor-pointer bg-blue-500 block rounded hover:opacity-80"
>
Choose a file...
</
label
>
<
input
className=
"absolute inset-0 hidden"
ref=
{
fileInputRef
}
onChange=
{
handleFileInputChange
}
type=
"file"
id=
"files"
multiple=
{
true
}
accept=
"*"
/>
</
div
>
<
List
size=
"sm"
>
{
fileList
.
map
((
file
)
=>
(
<
ListItem
key=
{
file
.
name
}
>
{
file
.
name
}
</
ListItem
>
))
}
</
List
>
</>
)
}
{
state
.
selectedMode
===
"external-link"
&&
(
<>
<
Typography
className=
"!mb-1"
level=
"body2"
>
Link
</
Typography
>
<
Input
className=
"mb-2"
placeholder=
"File link"
value=
{
resourceCreate
.
externalLink
}
onChange=
{
handleExternalLinkChanged
}
fullWidth
/>
<
Typography
className=
"!mb-1"
level=
"body2"
>
File name
</
Typography
>
<
Input
className=
"mb-2"
placeholder=
"File name"
value=
{
resourceCreate
.
filename
}
onChange=
{
handleFileNameChanged
}
fullWidth
/>
<
Typography
className=
"!mb-1"
level=
"body2"
>
Type
</
Typography
>
<
Autocomplete
className=
"w-full"
size=
"sm"
placeholder=
"File type"
freeSolo=
{
true
}
options=
{
fileTypeAutocompleteOptions
}
onChange=
{
(
_
,
value
)
=>
handleFileTypeChanged
(
value
||
""
)
}
/>
</>
)
}
<
div
className=
"mt-2 w-full flex flex-row justify-end items-center space-x-1"
>
<
Button
variant=
"plain"
color=
"neutral"
onClick=
{
handleCloseDialog
}
>
Cancel
</
Button
>
<
Button
onClick=
{
handleConfirmBtnClick
}
loading=
{
state
.
uploadingFlag
}
disabled=
{
!
allowConfirmAction
()
}
>
Create
</
Button
>
</
div
>
</
div
>
</>
);
};
function
showCreateResourceDialog
(
props
:
Omit
<
Props
,
"destroy"
>
)
{
generateDialog
<
Props
>
(
{
dialogName
:
"create-resource-dialog"
,
},
CreateResourceDialog
,
props
);
}
export
default
showCreateResourceDialog
;
web/src/components/MemoEditor.tsx
View file @
dd5a23e3
...
...
@@ -12,6 +12,7 @@ import Selector from "./common/Selector";
import
Editor
,
{
EditorRefActions
}
from
"./Editor/Editor"
;
import
ResourceIcon
from
"./ResourceIcon"
;
import
showResourcesSelectorDialog
from
"./ResourcesSelectorDialog"
;
import
showCreateResourceDialog
from
"./CreateResourceDialog"
;
import
"../less/memo-editor.less"
;
const
listItemSymbolList
=
[
"- [ ] "
,
"- [x] "
,
"- [X] "
,
"* "
,
"- "
];
...
...
@@ -418,33 +419,11 @@ const MemoEditor = () => {
};
const
handleUploadFileBtnClick
=
()
=>
{
const
inputEl
=
document
.
createElement
(
"input"
);
inputEl
.
style
.
position
=
"fixed"
;
inputEl
.
style
.
top
=
"-100vh"
;
inputEl
.
style
.
left
=
"-100vw"
;
document
.
body
.
appendChild
(
inputEl
);
inputEl
.
type
=
"file"
;
inputEl
.
multiple
=
true
;
inputEl
.
accept
=
"*"
;
inputEl
.
onchange
=
async
()
=>
{
if
(
!
inputEl
.
files
||
inputEl
.
files
.
length
===
0
)
{
return
;
}
const
resourceList
:
Resource
[]
=
[];
for
(
const
file
of
inputEl
.
files
)
{
const
resource
=
await
handleUploadResource
(
file
);
if
(
resource
)
{
resourceList
.
push
(
resource
);
if
(
editorState
.
editMemoId
)
{
await
upsertMemoResource
(
editorState
.
editMemoId
,
resource
.
id
);
}
}
}
editorStore
.
setResourceList
([...
editorState
.
resourceList
,
...
resourceList
]);
document
.
body
.
removeChild
(
inputEl
);
};
inputEl
.
click
();
showCreateResourceDialog
({
onConfirm
:
(
resourceList
)
=>
{
editorStore
.
setResourceList
([...
editorState
.
resourceList
,
...
resourceList
]);
},
});
};
const
handleFullscreenBtnClick
=
()
=>
{
...
...
@@ -536,7 +515,6 @@ const MemoEditor = () => {
</
button
>
<
div
className=
"action-btn resource-btn"
>
<
Icon
.
FileText
className=
"icon-img"
/>
<
span
className=
{
`tip-text ${state.isUploadingResource ? "!block" : ""}`
}
>
Uploading
</
span
>
<
div
className=
"resource-action-list"
>
<
div
className=
"resource-action-item"
onClick=
{
handleUploadFileBtnClick
}
>
<
Icon
.
Upload
className=
"icon-img"
/>
...
...
web/src/components/ResourcesDialog.tsx
View file @
dd5a23e3
...
...
@@ -11,6 +11,7 @@ import Dropdown from "./common/Dropdown";
import
{
generateDialog
}
from
"./Dialog"
;
import
{
showCommonDialog
}
from
"./Dialog/CommonDialog"
;
import
showPreviewImageDialog
from
"./PreviewImageDialog"
;
import
showCreateResourceDialog
from
"./CreateResourceDialog"
;
import
showChangeResourceFilenameDialog
from
"./ChangeResourceFilenameDialog"
;
import
"../less/resources-dialog.less"
;
...
...
@@ -35,34 +36,6 @@ const ResourcesDialog: React.FC<Props> = (props: Props) => {
});
},
[]);
const
handleUploadFileBtnClick
=
async
()
=>
{
const
inputEl
=
document
.
createElement
(
"input"
);
inputEl
.
style
.
position
=
"fixed"
;
inputEl
.
style
.
top
=
"-100vh"
;
inputEl
.
style
.
left
=
"-100vw"
;
document
.
body
.
appendChild
(
inputEl
);
inputEl
.
type
=
"file"
;
inputEl
.
multiple
=
true
;
inputEl
.
accept
=
"*"
;
inputEl
.
onchange
=
async
()
=>
{
if
(
!
inputEl
.
files
||
inputEl
.
files
.
length
===
0
)
{
return
;
}
for
(
const
file
of
inputEl
.
files
)
{
try
{
await
resourceStore
.
createResourceWithBlob
(
file
);
}
catch
(
error
:
any
)
{
console
.
error
(
error
);
toastHelper
.
error
(
error
.
response
.
data
.
message
);
}
}
document
.
body
.
removeChild
(
inputEl
);
};
inputEl
.
click
();
};
const
handlePreviewBtnClick
=
(
resource
:
Resource
)
=>
{
const
resourceUrl
=
getResourceUrl
(
resource
);
if
(
resource
.
type
.
startsWith
(
"image"
))
{
...
...
@@ -139,7 +112,7 @@ const ResourcesDialog: React.FC<Props> = (props: Props) => {
<
div
className=
"dialog-content-container"
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
div
className=
"flex flex-row justify-start items-center space-x-2"
>
<
Button
onClick=
{
()
=>
handleUploadFileBtnClick
(
)
}
startDecorator=
{
<
Icon
.
Plus
className=
"w-5 h-auto"
/>
}
>
<
Button
onClick=
{
()
=>
showCreateResourceDialog
({}
)
}
startDecorator=
{
<
Icon
.
Plus
className=
"w-5 h-auto"
/>
}
>
{
t
(
"common.create"
)
}
</
Button
>
</
div
>
...
...
web/src/types/modules/resource.d.ts
View file @
dd5a23e3
...
...
@@ -17,6 +17,7 @@ interface Resource {
interface
ResourceCreate
{
filename
:
string
;
externalLink
:
string
;
type
:
string
;
}
interface
ResourcePatch
{
...
...
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