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
b2c22977
Commit
b2c22977
authored
Sep 30, 2022
by
steven
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: update memo editor with uploading resources
parent
c0edb72b
Changes
6
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
243 additions
and
235 deletions
+243
-235
Editor.tsx
web/src/components/Editor/Editor.tsx
+5
-47
MemoEditor.tsx
web/src/components/MemoEditor.tsx
+151
-136
api.ts
web/src/helpers/api.ts
+14
-0
editor.less
web/src/less/editor.less
+0
-40
memo-editor.less
web/src/less/memo-editor.less
+72
-12
memoService.ts
web/src/services/memoService.ts
+1
-0
No files found.
web/src/components/Editor/Editor.tsx
View file @
b2c22977
import
{
forwardRef
,
ReactNode
,
useCallback
,
useEffect
,
useImperativeHandle
,
useRef
}
from
"react"
;
import
{
useTranslation
}
from
"react-i18next"
;
import
useRefresh
from
"../../hooks/useRefresh"
;
import
"../../less/editor.less"
;
export
interface
EditorRefActions
{
element
:
HTMLTextAreaElement
;
focus
:
FunctionType
;
insertText
:
(
text
:
string
)
=>
void
;
setContent
:
(
text
:
string
)
=>
void
;
...
...
@@ -12,29 +10,19 @@ export interface EditorRefActions {
getCursorPosition
:
()
=>
number
;
}
interface
Editor
Props
{
interface
Props
{
className
:
string
;
initialContent
:
string
;
placeholder
:
string
;
fullscreen
:
boolean
;
showConfirmBtn
:
boolean
;
tools
?:
ReactNode
;
on
ConfirmBtnClick
:
(
content
:
string
)
=>
void
;
on
Paste
:
(
event
:
React
.
ClipboardEvent
)
=>
void
;
onContentChange
:
(
content
:
string
)
=>
void
;
}
// eslint-disable-next-line react/display-name
const
Editor
=
forwardRef
((
props
:
EditorProps
,
ref
:
React
.
ForwardedRef
<
EditorRefActions
>
)
=>
{
const
{
className
,
initialContent
,
placeholder
,
fullscreen
,
showConfirmBtn
,
onConfirmBtnClick
:
handleConfirmBtnClickCallback
,
onContentChange
:
handleContentChangeCallback
,
}
=
props
;
const
{
t
}
=
useTranslation
();
const
Editor
=
forwardRef
((
props
:
Props
,
ref
:
React
.
ForwardedRef
<
EditorRefActions
>
)
=>
{
const
{
className
,
initialContent
,
placeholder
,
fullscreen
,
onPaste
,
onContentChange
:
handleContentChangeCallback
}
=
props
;
const
editorRef
=
useRef
<
HTMLTextAreaElement
>
(
null
);
const
refresh
=
useRefresh
();
...
...
@@ -54,7 +42,6 @@ const Editor = forwardRef((props: EditorProps, ref: React.ForwardedRef<EditorRef
useImperativeHandle
(
ref
,
()
=>
({
element
:
editorRef
.
current
as
HTMLTextAreaElement
,
focus
:
()
=>
{
editorRef
.
current
?.
focus
();
},
...
...
@@ -94,25 +81,6 @@ const Editor = forwardRef((props: EditorProps, ref: React.ForwardedRef<EditorRef
refresh
();
},
[]);
const
handleEditorKeyDown
=
useCallback
((
event
:
React
.
KeyboardEvent
<
HTMLTextAreaElement
>
)
=>
{
event
.
stopPropagation
();
if
(
event
.
code
===
"Enter"
)
{
if
(
event
.
metaKey
||
event
.
ctrlKey
)
{
handleCommonConfirmBtnClick
();
}
}
},
[]);
const
handleCommonConfirmBtnClick
=
useCallback
(()
=>
{
if
(
!
editorRef
.
current
)
{
return
;
}
handleConfirmBtnClickCallback
(
editorRef
.
current
.
value
);
editorRef
.
current
.
value
=
""
;
},
[]);
return
(
<
div
className=
{
"common-editor-wrapper "
+
className
}
>
<
textarea
...
...
@@ -120,19 +88,9 @@ const Editor = forwardRef((props: EditorProps, ref: React.ForwardedRef<EditorRef
rows=
{
1
}
placeholder=
{
placeholder
}
ref=
{
editorRef
}
onPaste=
{
onPaste
}
onInput=
{
handleEditorInput
}
onKeyDown=
{
handleEditorKeyDown
}
></
textarea
>
<
div
className=
"common-tools-wrapper"
>
<
div
className=
"common-tools-container"
>
{
props
.
tools
!==
undefined
&&
props
.
tools
}
</
div
>
<
div
className=
"btns-container"
>
{
showConfirmBtn
&&
(
<
button
className=
"action-btn confirm-btn"
disabled=
{
editorRef
.
current
?.
value
===
""
}
onClick=
{
handleCommonConfirmBtnClick
}
>
{
t
(
"editor.save"
)
}
<
span
className=
"icon-text"
>
✍️
</
span
>
</
button
>
)
}
</
div
>
</
div
>
</
div
>
);
});
...
...
web/src/components/MemoEditor.tsx
View file @
b2c22977
import
{
IEmojiData
}
from
"emoji-picker-react"
;
import
React
,
{
useCallback
,
useEffect
,
useMemo
,
useRef
,
useState
}
from
"react"
;
import
{
useTranslation
}
from
"react-i18next"
;
import
{
deleteMemoResource
,
getMemoResourceList
,
upsertMemoResource
}
from
"../helpers/api"
;
import
{
UNKNOWN_ID
}
from
"../helpers/consts"
;
import
{
editorStateService
,
locationService
,
memoService
,
resourceService
}
from
"../services"
;
import
{
useAppSelector
}
from
"../store"
;
...
...
@@ -25,6 +26,7 @@ interface State {
fullscreen
:
boolean
;
isUploadingResource
:
boolean
;
shouldShowEmojiPicker
:
boolean
;
resourceList
:
Resource
[];
}
const
MemoEditor
:
React
.
FC
=
()
=>
{
...
...
@@ -36,9 +38,11 @@ const MemoEditor: React.FC = () => {
isUploadingResource
:
false
,
fullscreen
:
false
,
shouldShowEmojiPicker
:
false
,
resourceList
:
[],
});
const
editorRef
=
useRef
<
EditorRefActions
>
(
null
);
const
[
allowSave
,
setAllowSave
]
=
useState
<
boolean
>
(
false
);
const
prevGlobalStateRef
=
useRef
(
editorState
);
const
editorRef
=
useRef
<
EditorRefActions
>
(
null
);
const
tagSeletorRef
=
useRef
<
HTMLDivElement
>
(
null
);
const
editorFontStyle
=
user
?.
setting
.
editorFontStyle
||
"normal"
;
const
mobileEditorStyle
=
user
?.
setting
.
mobileEditorStyle
||
"normal"
;
...
...
@@ -50,104 +54,69 @@ const MemoEditor: React.FC = () => {
editorRef
.
current
?.
insertText
(
memoLinkText
);
editorStateService
.
clearMarkMemo
();
}
},
[
editorState
.
markMemoId
]);
useEffect
(()
=>
{
if
(
editorState
.
editMemoId
&&
editorState
.
editMemoId
!==
UNKNOWN_ID
&&
editorState
.
editMemoId
!==
prevGlobalStateRef
.
current
.
editMemoId
)
{
const
editMemo
=
memoService
.
getMemoById
(
editorState
.
editMemoId
??
UNKNOWN_ID
);
if
(
editMemo
)
{
editorRef
.
current
?.
setContent
(
editMemo
.
content
??
""
);
const
memo
=
memoService
.
getMemoById
(
editorState
.
editMemoId
??
UNKNOWN_ID
);
if
(
memo
)
{
getMemoResourceList
(
memo
.
id
).
then
(({
data
:
{
data
}
})
=>
{
setState
({
...
state
,
resourceList
:
data
,
});
});
editorRef
.
current
?.
setContent
(
memo
.
content
??
""
);
editorRef
.
current
?.
focus
();
}
}
prevGlobalStateRef
.
current
=
editorState
;
},
[
editorState
.
markMemoId
,
editorState
.
editMemoId
]);
useEffect
(()
=>
{
const
handlePasteEvent
=
async
(
event
:
ClipboardEvent
)
=>
{
if
(
event
.
clipboardData
&&
event
.
clipboardData
.
files
.
length
>
0
)
{
event
.
preventDefault
();
const
file
=
event
.
clipboardData
.
files
[
0
];
const
url
=
await
handleUploadFile
(
file
);
if
(
url
)
{
editorRef
.
current
?.
insertText
(
``
);
}
}
};
const
handleDropEvent
=
async
(
event
:
DragEvent
)
=>
{
if
(
event
.
dataTransfer
&&
event
.
dataTransfer
.
files
.
length
>
0
)
{
event
.
preventDefault
();
const
file
=
event
.
dataTransfer
.
files
[
0
];
const
url
=
await
handleUploadFile
(
file
);
if
(
url
)
{
editorRef
.
current
?.
insertText
(
``
);
}
},
[
state
,
editorState
.
editMemoId
]);
const
handlePasteEvent
=
async
(
event
:
React
.
ClipboardEvent
)
=>
{
if
(
event
.
clipboardData
&&
event
.
clipboardData
.
files
.
length
>
0
)
{
event
.
preventDefault
();
const
file
=
event
.
clipboardData
.
files
[
0
];
const
resource
=
await
handleUploadResource
(
file
);
if
(
resource
)
{
setState
({
...
state
,
resourceList
:
[...
state
.
resourceList
,
resource
],
});
}
};
const
handleClickEvent
=
()
=>
{
handleContentChange
(
editorRef
.
current
?.
element
.
value
??
""
);
};
const
handleKeyDownEvent
=
()
=>
{
setTimeout
(()
=>
{
handleContentChange
(
editorRef
.
current
?.
element
.
value
??
""
);
});
};
editorRef
.
current
?.
element
.
addEventListener
(
"paste"
,
handlePasteEvent
);
editorRef
.
current
?.
element
.
addEventListener
(
"drop"
,
handleDropEvent
);
editorRef
.
current
?.
element
.
addEventListener
(
"click"
,
handleClickEvent
);
editorRef
.
current
?.
element
.
addEventListener
(
"keydown"
,
handleKeyDownEvent
);
return
()
=>
{
editorRef
.
current
?.
element
.
removeEventListener
(
"paste"
,
handlePasteEvent
);
editorRef
.
current
?.
element
.
removeEventListener
(
"drop"
,
handleDropEvent
);
editorRef
.
current
?.
element
.
removeEventListener
(
"click"
,
handleClickEvent
);
editorRef
.
current
?.
element
.
removeEventListener
(
"keydown"
,
handleKeyDownEvent
);
};
},
[]);
}
};
const
handleUpload
File
=
useCallback
(
async
(
file
:
File
)
=>
{
if
(
state
.
isUploadingResource
)
{
return
;
}
const
handleUpload
Resource
=
async
(
file
:
File
)
=>
{
setState
(
{
...
state
,
isUploadingResource
:
true
,
});
setState
({
...
state
,
isUploadingResource
:
true
,
});
const
{
type
}
=
file
;
let
resource
=
undefined
;
if
(
!
type
.
startsWith
(
"image"
))
{
toastHelper
.
error
(
t
(
"editor.only-image-supported"
));
return
;
}
try
{
resource
=
await
resourceService
.
upload
(
file
);
}
catch
(
error
:
any
)
{
console
.
error
(
error
);
toastHelper
.
error
(
error
.
response
.
data
.
message
);
}
try
{
const
image
=
await
resourceService
.
upload
(
file
);
const
url
=
`/o/r/
${
image
.
id
}
/
${
image
.
filename
}
`
;
return
url
;
}
catch
(
error
:
any
)
{
console
.
error
(
error
);
toastHelper
.
error
(
error
.
response
.
data
.
message
);
}
finally
{
setState
({
...
state
,
isUploadingResource
:
false
,
});
}
},
[
state
]
);
setState
({
...
state
,
isUploadingResource
:
false
,
});
return
resource
;
};
const
handleSaveBtnClick
=
async
(
content
:
string
)
=>
{
if
(
content
===
""
)
{
const
handleSaveBtnClick
=
async
()
=>
{
const
content
=
editorRef
.
current
?.
getContent
();
if
(
!
content
)
{
toastHelper
.
error
(
t
(
"editor.cant-empty"
));
return
;
}
...
...
@@ -165,9 +134,12 @@ const MemoEditor: React.FC = () => {
}
editorStateService
.
clearEditMemo
();
}
else
{
await
memoService
.
createMemo
({
const
memo
=
await
memoService
.
createMemo
({
content
,
});
for
(
const
resource
of
state
.
resourceList
)
{
await
upsertMemoResource
(
memo
.
id
,
resource
.
id
);
}
locationService
.
clearQuery
();
}
}
catch
(
error
:
any
)
{
...
...
@@ -178,19 +150,26 @@ const MemoEditor: React.FC = () => {
setState
({
...
state
,
fullscreen
:
false
,
resourceList
:
[],
});
setEditorContentCache
(
""
);
editorRef
.
current
?.
setContent
(
""
);
};
const
handleCancelEditingBtnClick
=
useCallback
(()
=>
{
const
handleCancelEditing
=
()
=>
{
setState
({
...
state
,
resourceList
:
[],
});
editorStateService
.
clearEditMemo
();
editorRef
.
current
?.
setContent
(
""
);
setEditorContentCache
(
""
);
}
,
[])
;
};
const
handleContentChange
=
useCallback
((
content
:
string
)
=>
{
const
handleContentChange
=
(
content
:
string
)
=>
{
setAllowSave
(
content
!==
""
);
setEditorContentCache
(
content
);
}
,
[])
;
};
const
handleEmojiPickerBtnClick
=
()
=>
{
handleChangeShouldShowEmojiPicker
(
!
state
.
shouldShowEmojiPicker
);
...
...
@@ -224,7 +203,7 @@ const MemoEditor: React.FC = () => {
}
};
const
handleUploadFileBtnClick
=
useCallback
(
()
=>
{
const
handleUploadFileBtnClick
=
()
=>
{
const
inputEl
=
document
.
createElement
(
"input"
);
inputEl
.
style
.
position
=
"fixed"
;
inputEl
.
style
.
top
=
"-100vh"
;
...
...
@@ -232,22 +211,30 @@ const MemoEditor: React.FC = () => {
document
.
body
.
appendChild
(
inputEl
);
inputEl
.
type
=
"file"
;
inputEl
.
multiple
=
true
;
inputEl
.
accept
=
"
image/
*"
;
inputEl
.
accept
=
"*"
;
inputEl
.
onchange
=
async
()
=>
{
if
(
!
inputEl
.
files
||
inputEl
.
files
.
length
===
0
)
{
return
;
}
const
resourceList
:
Resource
[]
=
[];
for
(
const
file
of
inputEl
.
files
)
{
const
url
=
await
handleUploadFile
(
file
);
if
(
url
)
{
editorRef
.
current
?.
insertText
(
``
);
const
resource
=
await
handleUploadResource
(
file
);
if
(
resource
)
{
resourceList
.
push
(
resource
);
if
(
editorState
.
editMemoId
)
{
await
upsertMemoResource
(
editorState
.
editMemoId
,
resource
.
id
);
}
}
}
setState
({
...
state
,
resourceList
:
[...
state
.
resourceList
,
...
resourceList
],
});
document
.
body
.
removeChild
(
inputEl
);
};
inputEl
.
click
();
}
,
[])
;
};
const
handleFullscreenBtnClick
=
()
=>
{
setState
({
...
...
@@ -279,6 +266,17 @@ const MemoEditor: React.FC = () => {
handleChangeShouldShowEmojiPicker
(
false
);
};
const
handleDeleteResource
=
async
(
resourceId
:
ResourceId
)
=>
{
setState
({
...
state
,
resourceList
:
state
.
resourceList
.
filter
((
resource
)
=>
resource
.
id
!==
resourceId
),
});
if
(
editorState
.
editMemoId
)
{
await
deleteMemoResource
(
editorState
.
editMemoId
,
resourceId
);
}
};
const
isEditing
=
Boolean
(
editorState
.
editMemoId
&&
editorState
.
editMemoId
!==
UNKNOWN_ID
);
const
editorConfig
=
useMemo
(
...
...
@@ -287,63 +285,80 @@ const MemoEditor: React.FC = () => {
initialContent
:
getEditorContentCache
(),
placeholder
:
t
(
"editor.placeholder"
),
fullscreen
:
state
.
fullscreen
,
showConfirmBtn
:
true
,
onConfirmBtnClick
:
handleSaveBtnClick
,
onContentChange
:
handleContentChange
,
}),
[
isEditing
,
state
.
fullscreen
,
i18n
.
language
,
editorFontStyle
]
[
state
.
fullscreen
,
i18n
.
language
,
editorFontStyle
]
);
return
(
<
div
className=
{
`memo-editor-container ${mobileEditorStyle} ${isEditing ? "edit-ing" : ""} ${state.fullscreen ? "fullscreen" : ""}`
}
>
<
div
className=
{
`tip-container ${isEditing ? "" : "!hidden"}`
}
>
<
span
className=
"tip-text"
>
{
t
(
"editor.editing"
)
}
</
span
>
<
button
className=
"cancel-btn"
onClick=
{
handleCancelEditing
BtnClick
}
>
<
button
className=
"cancel-btn"
onClick=
{
handleCancelEditing
}
>
{
t
(
"common.cancel"
)
}
</
button
>
</
div
>
<
Editor
ref=
{
editorRef
}
{
...
editorConfig
}
tools=
{
<>
<
div
className=
"action-btn tag-action"
>
<
Icon
.
Hash
className=
"icon-img"
/>
<
div
ref=
{
tagSeletorRef
}
className=
"tag-list"
onClick=
{
handleTagSeletorClick
}
>
{
tags
.
length
>
0
?
(
tags
.
map
((
tag
)
=>
{
return
(
<
span
className=
"item-container"
key=
{
tag
}
>
{
tag
}
</
span
>
);
})
<
Editor
ref=
{
editorRef
}
{
...
editorConfig
}
onPaste=
{
handlePasteEvent
}
/>
<
div
className=
"common-tools-wrapper"
>
<
div
className=
"common-tools-container"
>
<
div
className=
"action-btn tag-action"
>
<
Icon
.
Hash
className=
"icon-img"
/>
<
div
ref=
{
tagSeletorRef
}
className=
"tag-list"
onClick=
{
handleTagSeletorClick
}
>
{
tags
.
length
>
0
?
(
tags
.
map
((
tag
)
=>
{
return
(
<
span
className=
"item-container"
key=
{
tag
}
>
{
tag
}
</
span
>
);
})
)
:
(
<
p
className=
"tip-text"
onClick=
{
(
e
)
=>
e
.
stopPropagation
()
}
>
{
t
(
"common.null"
)
}
</
p
>
)
}
</
div
>
</
div
>
<
button
className=
"action-btn"
>
<
Icon
.
Smile
className=
"icon-img"
onClick=
{
handleEmojiPickerBtnClick
}
/>
</
button
>
<
button
className=
"action-btn"
>
<
Icon
.
CheckSquare
className=
"icon-img"
onClick=
{
handleCheckBoxBtnClick
}
/>
</
button
>
<
button
className=
"action-btn"
>
<
Icon
.
Code
className=
"icon-img"
onClick=
{
handleCodeBlockBtnClick
}
/>
</
button
>
<
button
className=
"action-btn"
>
<
Icon
.
FileText
className=
"icon-img"
onClick=
{
handleUploadFileBtnClick
}
/>
<
span
className=
{
`tip-text ${state.isUploadingResource ? "!block" : ""}`
}
>
Uploading
</
span
>
</
button
>
<
button
className=
"action-btn"
onClick=
{
handleFullscreenBtnClick
}
>
{
state
.
fullscreen
?
<
Icon
.
Minimize
className=
"icon-img"
/>
:
<
Icon
.
Maximize
className=
"icon-img"
/>
}
</
button
>
</
div
>
<
div
className=
"btns-container"
>
<
button
className=
"action-btn confirm-btn"
disabled=
{
!
allowSave
}
onClick=
{
handleSaveBtnClick
}
>
{
t
(
"editor.save"
)
}
<
span
className=
"icon-text"
>
✍️
</
span
>
</
button
>
</
div
>
</
div
>
{
state
.
resourceList
.
length
>
0
&&
(
<
div
className=
"resource-list-wrapper"
>
{
state
.
resourceList
.
map
((
resource
)
=>
{
return
(
<
div
key=
{
resource
.
id
}
className=
"resource-container"
>
{
resource
.
type
.
includes
(
"image"
)
?
(
<
Icon
.
Image
className=
"icon-img"
onClick=
{
handleUploadFileBtnClick
}
/>
)
:
(
<
p
className=
"tip-text"
onClick=
{
(
e
)
=>
e
.
stopPropagation
()
}
>
{
t
(
"common.null"
)
}
</
p
>
<
Icon
.
FileText
className=
"icon-img"
onClick=
{
handleUploadFileBtnClick
}
/>
)
}
<
span
className=
"name-text"
>
{
resource
.
filename
}
</
span
>
<
Icon
.
X
className=
"close-icon"
onClick=
{
()
=>
handleDeleteResource
(
resource
.
id
)
}
/>
</
div
>
</
div
>
<
button
className=
"action-btn"
>
<
Icon
.
Smile
className=
"icon-img"
onClick=
{
handleEmojiPickerBtnClick
}
/>
</
button
>
<
button
className=
"action-btn"
>
<
Icon
.
CheckSquare
className=
"icon-img"
onClick=
{
handleCheckBoxBtnClick
}
/>
</
button
>
<
button
className=
"action-btn"
>
<
Icon
.
Code
className=
"icon-img"
onClick=
{
handleCodeBlockBtnClick
}
/>
</
button
>
<
button
className=
"action-btn"
>
<
Icon
.
Image
className=
"icon-img"
onClick=
{
handleUploadFileBtnClick
}
/>
<
span
className=
{
`tip-text ${state.isUploadingResource ? "!block" : ""}`
}
>
Uploading
</
span
>
</
button
>
<
button
className=
"action-btn"
onClick=
{
handleFullscreenBtnClick
}
>
{
state
.
fullscreen
?
<
Icon
.
Minimize
className=
"icon-img"
/>
:
<
Icon
.
Maximize
className=
"icon-img"
/>
}
</
button
>
</>
}
/>
);
})
}
</
div
>
)
}
<
EmojiPicker
shouldShow=
{
state
.
shouldShowEmojiPicker
}
onEmojiClick=
{
handleEmojiClick
}
...
...
web/src/helpers/api.ts
View file @
b2c22977
...
...
@@ -133,6 +133,20 @@ export function deleteResourceById(id: ResourceId) {
return
axios
.
delete
(
`/api/resource/
${
id
}
`
);
}
export
function
getMemoResourceList
(
memoId
:
MemoId
)
{
return
axios
.
get
<
ResponseObject
<
Resource
[]
>>
(
`/api/memo/
${
memoId
}
/resource`
);
}
export
function
upsertMemoResource
(
memoId
:
MemoId
,
resourceId
:
ResourceId
)
{
return
axios
.
post
<
ResponseObject
<
Resource
>>
(
`/api/memo/
${
memoId
}
/resource`
,
{
resourceId
,
});
}
export
function
deleteMemoResource
(
memoId
:
MemoId
,
resourceId
:
ResourceId
)
{
return
axios
.
delete
(
`/api/memo/
${
memoId
}
/resource/
${
resourceId
}
`
);
}
export
function
getTagList
(
tagFind
?:
TagFind
)
{
const
queryList
=
[];
if
(
tagFind
?.
creatorId
)
{
...
...
web/src/less/editor.less
View file @
b2c22977
...
...
@@ -25,44 +25,4 @@
}
}
}
> .common-tools-wrapper {
@apply w-full flex flex-row justify-between items-center;
> .common-tools-container {
@apply flex flex-row justify-start items-center;
> .action-btn {
@apply flex flex-row justify-center items-center p-1 w-auto h-auto mr-1 select-none rounded cursor-pointer opacity-60 hover:opacity-90 hover:bg-gray-300 hover:shadow;
> .icon-img {
@apply w-5 h-5 mx-auto flex flex-row justify-center items-center;
}
> .tip-text {
@apply hidden ml-1 text-xs leading-5 text-gray-700 border border-gray-300 rounded-xl px-2;
}
}
}
> .btns-container {
@apply grow-0 shrink-0 flex flex-row justify-end items-center;
> .action-btn {
@apply border-none select-none cursor-pointer py-1 px-3 rounded text-sm hover:opacity-80;
}
> .cancel-btn {
@apply text-gray-500 bg-transparent mr-2;
}
> .confirm-btn {
@apply shadow cursor-pointer px-3 py-0 leading-8 bg-green-600 text-white text-sm hover:opacity-80 disabled:cursor-not-allowed disabled:opacity-60;
> .icon-text {
@apply text-base ml-1;
}
}
}
}
}
web/src/less/memo-editor.less
View file @
b2c22977
...
...
@@ -63,28 +63,88 @@
> .memo-editor {
@apply flex flex-col justify-start items-start relative w-full h-auto bg-white;
}
.tag-action
{
@apply relative
;
> .common-tools-wrapper
{
@apply w-full flex flex-row justify-between items-center
;
&:hov
er {
> .tag-list {
@apply flex;
}
}
> .common-tools-contain
er {
@apply flex flex-row justify-start items-center;
> .action-btn {
@apply flex flex-row justify-center items-center p-1 w-auto h-auto mr-1 select-none rounded cursor-pointer opacity-60 hover:opacity-90 hover:bg-gray-300 hover:shadow;
> .tag-list
{
@apply hidden flex-col justify-start items-start absolute top-6 left-0 mt-1 p-1 z-1 rounded w-32 max-h-52 overflow-auto font-mono bg-black
;
&.tag-action
{
@apply relative
;
> .item-container {
@apply w-full text-white cursor-pointer rounded text-sm leading-6 px-2 hover:bg-gray-700;
&:hover {
> .tag-list {
@apply flex;
}
}
> .tag-list {
@apply hidden flex-col justify-start items-start absolute top-6 left-0 mt-1 p-1 z-1 rounded w-32 max-h-52 overflow-auto font-mono bg-black;
> .item-container {
@apply w-full text-white cursor-pointer rounded text-sm leading-6 px-2 hover:bg-gray-700;
}
> .tip-text {
@apply w-full text-sm text-gray-200 leading-6 px-2 cursor-default;
}
}
}
> .icon-img {
@apply w-5 h-5 mx-auto flex flex-row justify-center items-center;
}
> .tip-text {
@apply
w-full text-sm text-gray-200 leading-6 px-2 cursor-default
;
@apply
hidden ml-1 text-xs leading-5 text-gray-700 border border-gray-300 rounded-xl px-2
;
}
}
}
> .btns-container {
@apply grow-0 shrink-0 flex flex-row justify-end items-center;
> .action-btn {
@apply border-none select-none cursor-pointer py-1 px-3 rounded text-sm hover:opacity-80;
}
> .cancel-btn {
@apply text-gray-500 bg-transparent mr-2;
}
> .confirm-btn {
@apply shadow cursor-pointer px-3 py-0 leading-8 bg-green-600 text-white text-sm hover:opacity-80 disabled:cursor-not-allowed disabled:opacity-60;
> .icon-text {
@apply text-base ml-1;
}
}
}
}
> .resource-list-wrapper {
@apply w-full flex flex-row justify-start flex-wrap;
> .resource-container {
@apply mt-1 mr-1 flex flex-row justify-start items-center flex-nowrap bg-gray-50 px-2 py-1 rounded cursor-pointer hover:bg-gray-100;
> .icon-img {
@apply w-4 h-auto mr-1 text-gray-500;
}
> .name-text {
@apply text-gray-500 text-sm max-w-xs truncate font-mono;
}
> .close-icon {
@apply w-4 h-auto ml-1 text-gray-500 hover:text-gray-800;
}
}
}
.emoji-picker-react {
...
...
web/src/services/memoService.ts
View file @
b2c22977
...
...
@@ -93,6 +93,7 @@ const memoService = {
const
{
data
}
=
(
await
api
.
createMemo
(
memoCreate
)).
data
;
const
memo
=
convertResponseModelMemo
(
data
);
store
.
dispatch
(
createMemo
(
memo
));
return
memo
;
},
patchMemo
:
async
(
memoPatch
:
MemoPatch
):
Promise
<
Memo
>
=>
{
...
...
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