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
bc2d2d0c
Commit
bc2d2d0c
authored
Jan 26, 2024
by
Steven
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: support set embedded content in UI
parent
e1977df1
Changes
6
Show whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
181 additions
and
83 deletions
+181
-83
CreateMemoRelationDialog.tsx
web/src/components/CreateMemoRelationDialog.tsx
+11
-10
EmbeddedMemo.tsx
...c/components/MemoContent/EmbeddedContent/EmbeddedMemo.tsx
+22
-2
AddMemoRelationButton.tsx
...ponents/MemoEditor/ActionButton/AddMemoRelationButton.tsx
+68
-0
index.tsx
web/src/components/MemoEditor/index.tsx
+65
-71
context.ts
web/src/components/MemoEditor/types/context.ts
+14
-0
index.ts
web/src/components/MemoEditor/types/index.ts
+1
-0
No files found.
web/src/components/CreateMemoRelationDialog.tsx
View file @
bc2d2d0c
import
{
Autocomplete
,
AutocompleteOption
,
Button
,
Chip
,
IconButton
}
from
"@mui/joy"
;
import
{
Autocomplete
,
AutocompleteOption
,
Button
,
Ch
eckbox
,
Ch
ip
,
IconButton
}
from
"@mui/joy"
;
import
React
,
{
useState
}
from
"react"
;
import
React
,
{
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
{
toast
}
from
"react-hot-toast"
;
import
useDebounce
from
"react-use/lib/useDebounce"
;
import
useDebounce
from
"react-use/lib/useDebounce"
;
...
@@ -11,18 +11,18 @@ import { generateDialog } from "./Dialog";
...
@@ -11,18 +11,18 @@ import { generateDialog } from "./Dialog";
import
Icon
from
"./Icon"
;
import
Icon
from
"./Icon"
;
interface
Props
extends
DialogProps
{
interface
Props
extends
DialogProps
{
onCancel
?:
()
=>
void
;
onConfirm
:
(
memoIdList
:
number
[],
embedded
?:
boolean
)
=>
void
;
onConfirm
?:
(
memoIdList
:
number
[])
=>
void
;
}
}
const
CreateMemoRelationDialog
:
React
.
FC
<
Props
>
=
(
props
:
Props
)
=>
{
const
CreateMemoRelationDialog
:
React
.
FC
<
Props
>
=
(
props
:
Props
)
=>
{
const
{
destroy
,
onC
ancel
,
onC
onfirm
}
=
props
;
const
{
destroy
,
onConfirm
}
=
props
;
const
t
=
useTranslate
();
const
t
=
useTranslate
();
const
user
=
useCurrentUser
();
const
user
=
useCurrentUser
();
const
[
searchText
,
setSearchText
]
=
useState
<
string
>
(
""
);
const
[
searchText
,
setSearchText
]
=
useState
<
string
>
(
""
);
const
[
isFetching
,
setIsFetching
]
=
useState
<
boolean
>
(
true
);
const
[
isFetching
,
setIsFetching
]
=
useState
<
boolean
>
(
true
);
const
[
fetchedMemos
,
setFetchedMemos
]
=
useState
<
Memo
[]
>
([]);
const
[
fetchedMemos
,
setFetchedMemos
]
=
useState
<
Memo
[]
>
([]);
const
[
selectedMemos
,
setSelectedMemos
]
=
useState
<
Memo
[]
>
([]);
const
[
selectedMemos
,
setSelectedMemos
]
=
useState
<
Memo
[]
>
([]);
const
[
embedded
,
setEmbedded
]
=
useState
<
boolean
>
(
false
);
const
filteredMemos
=
fetchedMemos
.
filter
((
memo
)
=>
!
selectedMemos
.
includes
(
memo
));
const
filteredMemos
=
fetchedMemos
.
filter
((
memo
)
=>
!
selectedMemos
.
includes
(
memo
));
useDebounce
(
useDebounce
(
...
@@ -73,16 +73,14 @@ const CreateMemoRelationDialog: React.FC<Props> = (props: Props) => {
...
@@ -73,16 +73,14 @@ const CreateMemoRelationDialog: React.FC<Props> = (props: Props) => {
};
};
const
handleCloseDialog
=
()
=>
{
const
handleCloseDialog
=
()
=>
{
if
(
onCancel
)
{
onCancel
();
}
destroy
();
destroy
();
};
};
const
handleConfirmBtnClick
=
async
()
=>
{
const
handleConfirmBtnClick
=
async
()
=>
{
if
(
onConfirm
)
{
onConfirm
(
onConfirm
(
selectedMemos
.
map
((
memo
)
=>
memo
.
id
));
selectedMemos
.
map
((
memo
)
=>
memo
.
id
),
}
embedded
);
destroy
();
destroy
();
};
};
...
@@ -133,6 +131,9 @@ const CreateMemoRelationDialog: React.FC<Props> = (props: Props) => {
...
@@ -133,6 +131,9 @@ const CreateMemoRelationDialog: React.FC<Props> = (props: Props) => {
}
}
onChange=
{
(
_
,
value
)
=>
setSelectedMemos
(
value
)
}
onChange=
{
(
_
,
value
)
=>
setSelectedMemos
(
value
)
}
/>
/>
<
div
className=
"mt-3"
>
<
Checkbox
label=
{
"Use as Embedded Content"
}
checked=
{
embedded
}
onChange=
{
(
e
)
=>
setEmbedded
(
e
.
target
.
checked
)
}
/>
</
div
>
<
div
className=
"mt-4 w-full flex flex-row justify-end items-center space-x-1"
>
<
div
className=
"mt-4 w-full flex flex-row justify-end items-center space-x-1"
>
<
Button
variant=
"plain"
color=
"neutral"
onClick=
{
handleCloseDialog
}
>
<
Button
variant=
"plain"
color=
"neutral"
onClick=
{
handleCloseDialog
}
>
{
t
(
"common.cancel"
)
}
{
t
(
"common.cancel"
)
}
...
...
web/src/components/MemoContent/EmbeddedContent/EmbeddedMemo.tsx
View file @
bc2d2d0c
import
{
useContext
,
useEffect
}
from
"react"
;
import
{
useContext
,
useEffect
}
from
"react"
;
import
{
Link
}
from
"react-router-dom"
;
import
Icon
from
"@/components/Icon"
;
import
MemoResourceListView
from
"@/components/MemoResourceListView"
;
import
MemoResourceListView
from
"@/components/MemoResourceListView"
;
import
{
getDateTimeString
}
from
"@/helpers/datetime"
;
import
useLoading
from
"@/hooks/useLoading"
;
import
useLoading
from
"@/hooks/useLoading"
;
import
{
useMemoStore
}
from
"@/store/v1"
;
import
{
useMemoStore
}
from
"@/store/v1"
;
import
MemoContent
from
".."
;
import
MemoContent
from
".."
;
...
@@ -11,7 +14,7 @@ interface Props {
...
@@ -11,7 +14,7 @@ interface Props {
params
:
string
;
params
:
string
;
}
}
const
EmbeddedMemo
=
({
resourceId
}:
Props
)
=>
{
const
EmbeddedMemo
=
({
resourceId
,
params
:
paramsStr
}:
Props
)
=>
{
const
context
=
useContext
(
RendererContext
);
const
context
=
useContext
(
RendererContext
);
const
loadingState
=
useLoading
();
const
loadingState
=
useLoading
();
const
memoStore
=
useMemoStore
();
const
memoStore
=
useMemoStore
();
...
@@ -34,12 +37,29 @@ const EmbeddedMemo = ({ resourceId }: Props) => {
...
@@ -34,12 +37,29 @@ const EmbeddedMemo = ({ resourceId }: Props) => {
// Add the memo to the set of embedded memos. This is used to prevent infinite loops when a memo embeds itself.
// Add the memo to the set of embedded memos. This is used to prevent infinite loops when a memo embeds itself.
context
.
embeddedMemos
.
add
(
resourceName
);
context
.
embeddedMemos
.
add
(
resourceName
);
const
params
=
new
URLSearchParams
(
paramsStr
);
const
useInlineMode
=
params
.
has
(
"inline"
);
if
(
useInlineMode
)
{
return
(
return
(
<
div
className=
"w-full"
>
<
div
className=
"w-full"
>
<
MemoContent
nodes=
{
memo
.
nodes
}
memoId=
{
memo
.
id
}
embeddedMemos=
{
context
.
embeddedMemos
}
/>
<
MemoContent
nodes=
{
memo
.
nodes
}
memoId=
{
memo
.
id
}
embeddedMemos=
{
context
.
embeddedMemos
}
/>
<
MemoResourceListView
resources=
{
memo
.
resources
}
/>
<
MemoResourceListView
resources=
{
memo
.
resources
}
/>
</
div
>
</
div
>
);
);
}
return
(
<
div
className=
"relative flex flex-col justify-start items-start w-full p-4 pt-3 mb-2 bg-white dark:bg-zinc-800 rounded-lg border border-gray-200 dark:border-zinc-700"
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
span
className=
"text-sm text-gray-400 select-none"
>
{
getDateTimeString
(
memo
.
displayTime
)
}
</
span
>
<
Link
className=
"hover:opacity-80"
to=
{
`/m/${memo.name}`
}
unstable_viewTransition
>
<
Icon
.
ExternalLink
className=
"w-4 h-auto opacity-80"
/>
</
Link
>
</
div
>
<
MemoContent
nodes=
{
memo
.
nodes
}
memoId=
{
memo
.
id
}
embeddedMemos=
{
context
.
embeddedMemos
}
/>
<
MemoResourceListView
resources=
{
memo
.
resources
}
/>
</
div
>
);
};
};
export
default
EmbeddedMemo
;
export
default
EmbeddedMemo
;
web/src/components/MemoEditor/ActionButton/AddMemoRelationButton.tsx
0 → 100644
View file @
bc2d2d0c
import
{
IconButton
}
from
"@mui/joy"
;
import
{
uniqBy
}
from
"lodash-es"
;
import
{
useContext
}
from
"react"
;
import
toast
from
"react-hot-toast"
;
import
showCreateMemoRelationDialog
from
"@/components/CreateMemoRelationDialog"
;
import
Icon
from
"@/components/Icon"
;
import
{
UNKNOWN_ID
}
from
"@/helpers/consts"
;
import
{
useMemoStore
}
from
"@/store/v1"
;
import
{
MemoRelation_Type
}
from
"@/types/proto/api/v2/memo_relation_service"
;
import
{
EditorRefActions
}
from
"../Editor"
;
import
{
MemoEditorContext
}
from
"../types"
;
interface
Props
{
editorRef
:
React
.
RefObject
<
EditorRefActions
>
;
}
const
AddMemoRelationButton
=
(
props
:
Props
)
=>
{
const
{
editorRef
}
=
props
;
const
memoStore
=
useMemoStore
();
const
context
=
useContext
(
MemoEditorContext
);
const
handleAddMemoRelationBtnClick
=
()
=>
{
showCreateMemoRelationDialog
({
onConfirm
:
(
memoIdList
,
embedded
)
=>
{
// If embedded mode is enabled, embed the memo instead of creating a relation.
if
(
embedded
)
{
if
(
!
editorRef
.
current
)
{
toast
.
error
(
"Failed to embed memo"
);
return
;
}
const
cursorPosition
=
editorRef
.
current
.
getCursorPosition
();
const
prevValue
=
editorRef
.
current
.
getContent
().
slice
(
0
,
cursorPosition
);
if
(
prevValue
!==
""
&&
!
prevValue
.
endsWith
(
"
\n
"
))
{
editorRef
.
current
.
insertText
(
"
\n
"
);
}
for
(
const
memoId
of
memoIdList
)
{
const
memo
=
memoStore
.
getMemoById
(
memoId
);
editorRef
.
current
.
insertText
(
`![[memos/
${
memo
.
name
}
]]\n`
);
}
setTimeout
(()
=>
{
editorRef
.
current
?.
scrollToCursor
();
editorRef
.
current
?.
focus
();
});
return
;
}
context
.
setRelationList
(
uniqBy
(
[
...
memoIdList
.
map
((
id
)
=>
({
memoId
:
context
.
memoId
||
UNKNOWN_ID
,
relatedMemoId
:
id
,
type
:
MemoRelation_Type
.
REFERENCE
})),
...
context
.
relationList
,
].
filter
((
relation
)
=>
relation
.
relatedMemoId
!==
(
context
.
memoId
||
UNKNOWN_ID
)),
"relatedMemoId"
)
);
},
});
};
return
(
<
IconButton
size=
"sm"
onClick=
{
handleAddMemoRelationBtnClick
}
>
<
Icon
.
Link
className=
"w-5 h-5 mx-auto"
/>
</
IconButton
>
);
};
export
default
AddMemoRelationButton
;
web/src/components/MemoEditor/index.tsx
View file @
bc2d2d0c
import
{
Select
,
Option
,
Button
,
IconButton
,
Divider
}
from
"@mui/joy"
;
import
{
Select
,
Option
,
Button
,
IconButton
,
Divider
}
from
"@mui/joy"
;
import
{
uniqBy
}
from
"lodash-es"
;
import
React
,
{
useEffect
,
useMemo
,
useRef
,
useState
}
from
"react"
;
import
React
,
{
useEffect
,
useMemo
,
useRef
,
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
{
toast
}
from
"react-hot-toast"
;
import
{
useTranslation
}
from
"react-i18next"
;
import
{
useTranslation
}
from
"react-i18next"
;
...
@@ -15,16 +14,17 @@ import { Resource } from "@/types/proto/api/v2/resource_service";
...
@@ -15,16 +14,17 @@ import { Resource } from "@/types/proto/api/v2/resource_service";
import
{
UserSetting
}
from
"@/types/proto/api/v2/user_service"
;
import
{
UserSetting
}
from
"@/types/proto/api/v2/user_service"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
convertVisibilityFromString
,
convertVisibilityToString
}
from
"@/utils/memo"
;
import
{
convertVisibilityFromString
,
convertVisibilityToString
}
from
"@/utils/memo"
;
import
showCreateMemoRelationDialog
from
"../CreateMemoRelationDialog"
;
import
showCreateResourceDialog
from
"../CreateResourceDialog"
;
import
showCreateResourceDialog
from
"../CreateResourceDialog"
;
import
Icon
from
"../Icon"
;
import
Icon
from
"../Icon"
;
import
VisibilityIcon
from
"../VisibilityIcon"
;
import
VisibilityIcon
from
"../VisibilityIcon"
;
import
AddMemoRelationButton
from
"./ActionButton/AddMemoRelationButton"
;
import
MarkdownMenu
from
"./ActionButton/MarkdownMenu"
;
import
MarkdownMenu
from
"./ActionButton/MarkdownMenu"
;
import
TagSelector
from
"./ActionButton/TagSelector"
;
import
TagSelector
from
"./ActionButton/TagSelector"
;
import
Editor
,
{
EditorRefActions
}
from
"./Editor"
;
import
Editor
,
{
EditorRefActions
}
from
"./Editor"
;
import
RelationListView
from
"./RelationListView"
;
import
RelationListView
from
"./RelationListView"
;
import
ResourceListView
from
"./ResourceListView"
;
import
ResourceListView
from
"./ResourceListView"
;
import
{
handleEditorKeydownWithMarkdownShortcuts
,
hyperlinkHighlightedText
}
from
"./handlers"
;
import
{
handleEditorKeydownWithMarkdownShortcuts
,
hyperlinkHighlightedText
}
from
"./handlers"
;
import
{
MemoEditorContext
}
from
"./types"
;
interface
Props
{
interface
Props
{
className
?:
string
;
className
?:
string
;
...
@@ -158,23 +158,6 @@ const MemoEditor = (props: Props) => {
...
@@ -158,23 +158,6 @@ const MemoEditor = (props: Props) => {
});
});
};
};
const
handleAddMemoRelationBtnClick
=
()
=>
{
showCreateMemoRelationDialog
({
onConfirm
:
(
memoIdList
)
=>
{
setState
((
prevState
)
=>
({
...
prevState
,
relationList
:
uniqBy
(
[
...
memoIdList
.
map
((
id
)
=>
({
memoId
:
memoId
||
UNKNOWN_ID
,
relatedMemoId
:
id
,
type
:
MemoRelation_Type
.
REFERENCE
})),
...
state
.
relationList
,
].
filter
((
relation
)
=>
relation
.
relatedMemoId
!==
(
memoId
||
UNKNOWN_ID
)),
"relatedMemoId"
),
}));
},
});
};
const
handleSetResourceList
=
(
resourceList
:
Resource
[])
=>
{
const
handleSetResourceList
=
(
resourceList
:
Resource
[])
=>
{
setState
((
prevState
)
=>
({
setState
((
prevState
)
=>
({
...
prevState
,
...
prevState
,
...
@@ -371,6 +354,18 @@ const MemoEditor = (props: Props) => {
...
@@ -371,6 +354,18 @@ const MemoEditor = (props: Props) => {
const
allowSave
=
(
hasContent
||
state
.
resourceList
.
length
>
0
)
&&
!
state
.
isUploadingResource
&&
!
state
.
isRequesting
;
const
allowSave
=
(
hasContent
||
state
.
resourceList
.
length
>
0
)
&&
!
state
.
isUploadingResource
&&
!
state
.
isRequesting
;
return
(
return
(
<
MemoEditorContext
.
Provider
value=
{
{
relationList
:
state
.
relationList
,
setRelationList
:
(
relationList
:
MemoRelation
[])
=>
{
setState
((
prevState
)
=>
({
...
prevState
,
relationList
,
}));
},
memoId
,
}
}
>
<
div
<
div
className=
{
`${
className=
{
`${
className ?? ""
className ?? ""
...
@@ -390,9 +385,7 @@ const MemoEditor = (props: Props) => {
...
@@ -390,9 +385,7 @@ const MemoEditor = (props: Props) => {
<
IconButton
size=
"sm"
onClick=
{
handleUploadFileBtnClick
}
>
<
IconButton
size=
"sm"
onClick=
{
handleUploadFileBtnClick
}
>
<
Icon
.
Image
className=
"w-5 h-5 mx-auto"
/>
<
Icon
.
Image
className=
"w-5 h-5 mx-auto"
/>
</
IconButton
>
</
IconButton
>
<
IconButton
size=
"sm"
onClick=
{
handleAddMemoRelationBtnClick
}
>
<
AddMemoRelationButton
editorRef=
{
editorRef
}
/>
<
Icon
.
Link
className=
"w-5 h-5 mx-auto"
/>
</
IconButton
>
</
div
>
</
div
>
</
div
>
</
div
>
<
Divider
className=
"!mt-2"
/>
<
Divider
className=
"!mt-2"
/>
...
@@ -427,6 +420,7 @@ const MemoEditor = (props: Props) => {
...
@@ -427,6 +420,7 @@ const MemoEditor = (props: Props) => {
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
</
MemoEditorContext
.
Provider
>
);
);
};
};
...
...
web/src/components/MemoEditor/types/context.ts
0 → 100644
View file @
bc2d2d0c
import
{
createContext
}
from
"react"
;
import
{
MemoRelation
}
from
"@/types/proto/api/v2/memo_relation_service"
;
interface
Context
{
relationList
:
MemoRelation
[];
setRelationList
:
(
relationList
:
MemoRelation
[])
=>
void
;
// memoId is the id of the memo that is being edited.
memoId
?:
number
;
}
export
const
MemoEditorContext
=
createContext
<
Context
>
({
relationList
:
[],
setRelationList
:
()
=>
{},
});
web/src/components/MemoEditor/types/index.ts
0 → 100644
View file @
bc2d2d0c
export
*
from
"./context"
;
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