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
68a77b6e
Unverified
Commit
68a77b6e
authored
Dec 21, 2022
by
boojack
Committed by
GitHub
Dec 21, 2022
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: create tag dialog (#814)
parent
e4a8a4d7
Changes
16
Hide whitespace changes
Inline
Side-by-side
Showing
16 changed files
with
361 additions
and
47 deletions
+361
-47
tag.go
server/tag.go
+99
-7
tag_test.go
server/tag_test.go
+2
-2
tag.go
store/tag.go
+3
-1
CreateShortcutDialog.tsx
web/src/components/CreateShortcutDialog.tsx
+3
-3
CreateTagDialog.tsx
web/src/components/CreateTagDialog.tsx
+140
-0
MemoEditor.tsx
web/src/components/MemoEditor.tsx
+3
-2
TagList.tsx
web/src/components/TagList.tsx
+15
-8
UserBanner.tsx
web/src/components/UserBanner.tsx
+4
-2
api.ts
web/src/helpers/api.ts
+14
-0
tag-list.less
web/src/less/tag-list.less
+0
-4
index.ts
web/src/store/index.ts
+2
-0
index.ts
web/src/store/module/index.ts
+1
-0
memo.ts
web/src/store/module/memo.ts
+1
-9
tag.ts
web/src/store/module/tag.ts
+31
-0
memo.ts
web/src/store/reducer/memo.ts
+1
-9
tag.ts
web/src/store/reducer/tag.ts
+42
-0
No files found.
server/tag.go
View file @
68a77b6e
...
@@ -2,20 +2,85 @@ package server
...
@@ -2,20 +2,85 @@ package server
import
(
import
(
"encoding/json"
"encoding/json"
"fmt"
"net/http"
"net/http"
"regexp"
"regexp"
"sort"
"sort"
"strconv"
"strconv"
"github.com/usememos/memos/api"
"github.com/usememos/memos/api"
"github.com/usememos/memos/common"
metric
"github.com/usememos/memos/plugin/metrics"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4"
)
)
var
tagRegexpList
=
[]
*
regexp
.
Regexp
{
regexp
.
MustCompile
(
`^#([^\s#]+?) `
),
regexp
.
MustCompile
(
`^#([^\s#]+?)\s`
),
regexp
.
MustCompile
(
`[^\S]#([^\s#]+?)$`
),
regexp
.
MustCompile
(
`[^\S]#([^\s#]+?) `
),
regexp
.
MustCompile
(
` #([^\s#]+?) `
)}
func
(
s
*
Server
)
registerTagRoutes
(
g
*
echo
.
Group
)
{
func
(
s
*
Server
)
registerTagRoutes
(
g
*
echo
.
Group
)
{
g
.
POST
(
"/tag"
,
func
(
c
echo
.
Context
)
error
{
ctx
:=
c
.
Request
()
.
Context
()
userID
,
ok
:=
c
.
Get
(
getUserIDContextKey
())
.
(
int
)
if
!
ok
{
return
echo
.
NewHTTPError
(
http
.
StatusUnauthorized
,
"Missing user in session"
)
}
tagUpsert
:=
&
api
.
TagUpsert
{
CreatorID
:
userID
,
}
if
err
:=
json
.
NewDecoder
(
c
.
Request
()
.
Body
)
.
Decode
(
tagUpsert
);
err
!=
nil
{
return
echo
.
NewHTTPError
(
http
.
StatusBadRequest
,
"Malformatted post tag request"
)
.
SetInternal
(
err
)
}
if
tagUpsert
.
Name
==
""
{
return
echo
.
NewHTTPError
(
http
.
StatusBadRequest
,
"Tag name shouldn't be empty"
)
}
tag
,
err
:=
s
.
Store
.
UpsertTag
(
ctx
,
tagUpsert
)
if
err
!=
nil
{
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
"Failed to upsert tag"
)
.
SetInternal
(
err
)
}
s
.
Collector
.
Collect
(
ctx
,
&
metric
.
Metric
{
Name
:
"tag created"
,
})
c
.
Response
()
.
Header
()
.
Set
(
echo
.
HeaderContentType
,
echo
.
MIMEApplicationJSONCharsetUTF8
)
if
err
:=
json
.
NewEncoder
(
c
.
Response
()
.
Writer
)
.
Encode
(
composeResponse
(
tag
.
Name
));
err
!=
nil
{
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
"Failed to encode tag response"
)
.
SetInternal
(
err
)
}
return
nil
})
g
.
GET
(
"/tag"
,
func
(
c
echo
.
Context
)
error
{
g
.
GET
(
"/tag"
,
func
(
c
echo
.
Context
)
error
{
ctx
:=
c
.
Request
()
.
Context
()
tagFind
:=
&
api
.
TagFind
{}
if
userID
,
err
:=
strconv
.
Atoi
(
c
.
QueryParam
(
"creatorId"
));
err
==
nil
{
tagFind
.
CreatorID
=
userID
}
if
tagFind
.
CreatorID
==
0
{
currentUserID
,
ok
:=
c
.
Get
(
getUserIDContextKey
())
.
(
int
)
if
!
ok
{
return
echo
.
NewHTTPError
(
http
.
StatusBadRequest
,
"Missing user id to find tag"
)
}
tagFind
.
CreatorID
=
currentUserID
}
tagList
,
err
:=
s
.
Store
.
FindTagList
(
ctx
,
tagFind
)
if
err
!=
nil
{
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
"Failed to find tag list"
)
.
SetInternal
(
err
)
}
tagNameList
:=
[]
string
{}
for
_
,
tag
:=
range
tagList
{
tagNameList
=
append
(
tagNameList
,
tag
.
Name
)
}
c
.
Response
()
.
Header
()
.
Set
(
echo
.
HeaderContentType
,
echo
.
MIMEApplicationJSONCharsetUTF8
)
if
err
:=
json
.
NewEncoder
(
c
.
Response
()
.
Writer
)
.
Encode
(
composeResponse
(
tagNameList
));
err
!=
nil
{
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
"Failed to encode tags response"
)
.
SetInternal
(
err
)
}
return
nil
})
g
.
GET
(
"/tag/suggestion"
,
func
(
c
echo
.
Context
)
error
{
ctx
:=
c
.
Request
()
.
Context
()
ctx
:=
c
.
Request
()
.
Context
()
contentSearch
:=
"#"
contentSearch
:=
"#"
normalRowStatus
:=
api
.
Normal
normalRowStatus
:=
api
.
Normal
...
@@ -65,15 +130,42 @@ func (s *Server) registerTagRoutes(g *echo.Group) {
...
@@ -65,15 +130,42 @@ func (s *Server) registerTagRoutes(g *echo.Group) {
}
}
return
nil
return
nil
})
})
g
.
DELETE
(
"/tag/:tagName"
,
func
(
c
echo
.
Context
)
error
{
ctx
:=
c
.
Request
()
.
Context
()
userID
,
ok
:=
c
.
Get
(
getUserIDContextKey
())
.
(
int
)
if
!
ok
{
return
echo
.
NewHTTPError
(
http
.
StatusUnauthorized
,
"Missing user in session"
)
}
tagName
:=
c
.
Param
(
"tagName"
)
if
tagName
==
""
{
return
echo
.
NewHTTPError
(
http
.
StatusBadRequest
,
"Tag name cannot be empty"
)
}
tagDelete
:=
&
api
.
TagDelete
{
Name
:
tagName
,
CreatorID
:
userID
,
}
if
err
:=
s
.
Store
.
DeleteTag
(
ctx
,
tagDelete
);
err
!=
nil
{
if
common
.
ErrorCode
(
err
)
==
common
.
NotFound
{
return
echo
.
NewHTTPError
(
http
.
StatusNotFound
,
fmt
.
Sprintf
(
"Tag name not found: %s"
,
tagName
))
}
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
fmt
.
Sprintf
(
"Failed to delete tag name: %v"
,
tagName
))
.
SetInternal
(
err
)
}
return
c
.
JSON
(
http
.
StatusOK
,
true
)
})
}
}
var
tagRegexp
=
regexp
.
MustCompile
(
`#([^\s#]+)`
)
func
findTagListFromMemoContent
(
memoContent
string
)
[]
string
{
func
findTagListFromMemoContent
(
memoContent
string
)
[]
string
{
tagMapSet
:=
make
(
map
[
string
]
bool
)
tagMapSet
:=
make
(
map
[
string
]
bool
)
for
_
,
tagRegexp
:=
range
tagRegexpList
{
matches
:=
tagRegexp
.
FindAllStringSubmatch
(
memoContent
,
-
1
)
for
_
,
rawTag
:=
range
tagRegexp
.
FindAllString
(
memoContent
,
-
1
)
{
for
_
,
v
:=
range
matches
{
tag
:=
tagRegexp
.
ReplaceAllString
(
rawTag
,
"$1"
)
tagName
:=
v
[
1
]
tagMapSet
[
tag
]
=
true
tagMapSet
[
tagName
]
=
true
}
}
}
tagList
:=
[]
string
{}
tagList
:=
[]
string
{}
...
...
server/tag_test.go
View file @
68a77b6e
...
@@ -31,11 +31,11 @@ func TestFindTagListFromMemoContent(t *testing.T) {
...
@@ -31,11 +31,11 @@ func TestFindTagListFromMemoContent(t *testing.T) {
},
},
{
{
memoContent
:
"#tag1 123123#tag2
\n
#tag3 #tag4 "
,
memoContent
:
"#tag1 123123#tag2
\n
#tag3 #tag4 "
,
want
:
[]
string
{
"tag1"
,
"tag3"
,
"tag4"
},
want
:
[]
string
{
"tag1"
,
"tag
2"
,
"tag
3"
,
"tag4"
},
},
},
{
{
memoContent
:
"#tag1 http://123123.com?123123#tag2
\n
#tag3 #tag4 http://123123.com?123123#tag2) "
,
memoContent
:
"#tag1 http://123123.com?123123#tag2
\n
#tag3 #tag4 http://123123.com?123123#tag2) "
,
want
:
[]
string
{
"tag1"
,
"tag3"
,
"tag4"
},
want
:
[]
string
{
"tag1"
,
"tag
2"
,
"tag2)"
,
"tag
3"
,
"tag4"
},
},
},
}
}
for
_
,
test
:=
range
tests
{
for
_
,
test
:=
range
tests
{
...
...
store/tag.go
View file @
68a77b6e
...
@@ -87,7 +87,9 @@ func upsertTag(ctx context.Context, tx *sql.Tx, upsert *api.TagUpsert) (*tagRaw,
...
@@ -87,7 +87,9 @@ func upsertTag(ctx context.Context, tx *sql.Tx, upsert *api.TagUpsert) (*tagRaw,
name, creator_id
name, creator_id
)
)
VALUES (?, ?)
VALUES (?, ?)
ON CONFLICT(name, creator_id) DO NOTHING
ON CONFLICT(name, creator_id) DO UPDATE
SET
name = EXCLUDED.name
RETURNING name, creator_id
RETURNING name, creator_id
`
`
var
tagRaw
tagRaw
var
tagRaw
tagRaw
...
...
web/src/components/CreateShortcutDialog.tsx
View file @
68a77b6e
import
dayjs
from
"dayjs"
;
import
dayjs
from
"dayjs"
;
import
{
useCallback
,
useEffect
,
useState
}
from
"react"
;
import
{
useCallback
,
useEffect
,
useState
}
from
"react"
;
import
{
useTranslation
}
from
"react-i18next"
;
import
{
useTranslation
}
from
"react-i18next"
;
import
{
use
MemoStore
,
useShortcut
Store
}
from
"../store/module"
;
import
{
use
ShortcutStore
,
useTag
Store
}
from
"../store/module"
;
import
{
filterConsts
,
getDefaultFilter
,
relationConsts
}
from
"../helpers/filter"
;
import
{
filterConsts
,
getDefaultFilter
,
relationConsts
}
from
"../helpers/filter"
;
import
useLoading
from
"../hooks/useLoading"
;
import
useLoading
from
"../hooks/useLoading"
;
import
Icon
from
"./Icon"
;
import
Icon
from
"./Icon"
;
...
@@ -162,9 +162,9 @@ interface MemoFilterInputerProps {
...
@@ -162,9 +162,9 @@ interface MemoFilterInputerProps {
const
MemoFilterInputer
:
React
.
FC
<
MemoFilterInputerProps
>
=
(
props
:
MemoFilterInputerProps
)
=>
{
const
MemoFilterInputer
:
React
.
FC
<
MemoFilterInputerProps
>
=
(
props
:
MemoFilterInputerProps
)
=>
{
const
{
index
,
filter
,
handleFilterChange
,
handleFilterRemove
}
=
props
;
const
{
index
,
filter
,
handleFilterChange
,
handleFilterRemove
}
=
props
;
const
{
t
}
=
useTranslation
();
const
{
t
}
=
useTranslation
();
const
memoStore
=
useMemo
Store
();
const
tagStore
=
useTag
Store
();
const
[
value
,
setValue
]
=
useState
<
string
>
(
filter
.
value
.
value
);
const
[
value
,
setValue
]
=
useState
<
string
>
(
filter
.
value
.
value
);
const
tags
=
Array
.
from
(
memo
Store
.
getState
().
tags
);
const
tags
=
Array
.
from
(
tag
Store
.
getState
().
tags
);
const
{
type
}
=
filter
;
const
{
type
}
=
filter
;
const
typeDataSource
=
Object
.
values
(
filterConsts
).
map
(({
text
,
value
})
=>
({
text
:
t
(
text
),
value
}));
const
typeDataSource
=
Object
.
values
(
filterConsts
).
map
(({
text
,
value
})
=>
({
text
:
t
(
text
),
value
}));
...
...
web/src/components/CreateTagDialog.tsx
0 → 100644
View file @
68a77b6e
import
{
TextField
}
from
"@mui/joy"
;
import
{
useEffect
,
useState
}
from
"react"
;
import
{
useTagStore
}
from
"../store/module"
;
import
{
getTagSuggestionList
}
from
"../helpers/api"
;
import
Tag
from
"../labs/marked/parser/Tag"
;
import
Icon
from
"./Icon"
;
import
toastHelper
from
"./Toast"
;
import
{
generateDialog
}
from
"./Dialog"
;
type
Props
=
DialogProps
;
const
validateTagName
=
(
tagName
:
string
):
boolean
=>
{
const
matchResult
=
Tag
.
matcher
(
`#
${
tagName
}
`
);
if
(
!
matchResult
||
matchResult
[
1
]
!==
tagName
)
{
return
false
;
}
return
true
;
};
const
CreateTagDialog
:
React
.
FC
<
Props
>
=
(
props
:
Props
)
=>
{
const
{
destroy
}
=
props
;
const
tagStore
=
useTagStore
();
const
[
tagName
,
setTagName
]
=
useState
<
string
>
(
""
);
const
[
suggestTagNameList
,
setSuggestTagNameList
]
=
useState
<
string
[]
>
([]);
const
tagNameList
=
tagStore
.
state
.
tags
;
useEffect
(()
=>
{
getTagSuggestionList
().
then
(({
data
})
=>
{
setSuggestTagNameList
(
data
.
data
.
filter
((
tag
)
=>
!
tagNameList
.
includes
(
tag
)
&&
validateTagName
(
tag
)));
});
},
[
tagNameList
]);
const
handleTagNameChanged
=
(
e
:
React
.
ChangeEvent
<
HTMLInputElement
>
)
=>
{
const
tagName
=
e
.
target
.
value
as
string
;
setTagName
(
tagName
.
trim
());
};
const
handleRemoveSuggestTag
=
(
tag
:
string
)
=>
{
setSuggestTagNameList
(
suggestTagNameList
.
filter
((
item
)
=>
item
!==
tag
));
};
const
handleSaveBtnClick
=
async
()
=>
{
if
(
!
validateTagName
(
tagName
))
{
toastHelper
.
error
(
"Invalid tag name"
);
return
;
}
try
{
await
tagStore
.
upsertTag
(
tagName
);
}
catch
(
error
:
any
)
{
console
.
error
(
error
);
toastHelper
.
error
(
error
.
response
.
data
.
message
);
}
};
const
handleDeleteTag
=
async
(
tag
:
string
)
=>
{
await
tagStore
.
deleteTag
(
tag
);
};
const
handleSaveSuggestTagList
=
async
()
=>
{
for
(
const
tagName
of
suggestTagNameList
)
{
if
(
validateTagName
(
tagName
))
{
await
tagStore
.
upsertTag
(
tagName
);
}
}
};
return
(
<>
<
div
className=
"dialog-header-container"
>
<
p
className=
"title-text"
>
Create Tag
</
p
>
<
button
className=
"btn close-btn"
onClick=
{
()
=>
destroy
()
}
>
<
Icon
.
X
/>
</
button
>
</
div
>
<
div
className=
"dialog-content-container !w-80"
>
<
TextField
className=
"mb-2"
placeholder=
"TAG_NAME"
value=
{
tagName
}
onChange=
{
handleTagNameChanged
}
fullWidth
startDecorator=
{
<
Icon
.
Hash
className=
"w-4 h-auto"
/>
}
endDecorator=
{
<
Icon
.
CheckCircle
onClick=
{
handleSaveBtnClick
}
className=
"w-4 h-auto"
/>
}
/>
{
tagNameList
.
length
>
0
&&
(
<>
<
p
className=
"w-full mt-2 mb-1 text-sm text-gray-400"
>
All tags
</
p
>
<
div
className=
"w-full flex flex-row justify-start items-start flex-wrap"
>
{
tagNameList
.
map
((
tag
)
=>
(
<
span
className=
"text-sm mr-2 mt-1 font-mono cursor-pointer truncate hover:opacity-60 hover:line-through"
key=
{
tag
}
onClick=
{
()
=>
handleDeleteTag
(
tag
)
}
>
#
{
tag
}
</
span
>
))
}
</
div
>
</>
)
}
{
suggestTagNameList
.
length
>
0
&&
(
<>
<
p
className=
"w-full mt-2 mb-1 text-sm text-gray-400"
>
Tag suggestions
</
p
>
<
div
className=
"w-full flex flex-row justify-start items-start flex-wrap"
>
{
suggestTagNameList
.
map
((
tag
)
=>
(
<
span
className=
"text-sm mr-2 mt-1 font-mono cursor-pointer truncate hover:opacity-60 hover:line-through"
key=
{
tag
}
onClick=
{
()
=>
handleRemoveSuggestTag
(
tag
)
}
>
#
{
tag
}
</
span
>
))
}
</
div
>
<
button
className=
"mt-2 text-sm border px-2 leading-6 rounded cursor-pointer hover:opacity-80 hover:shadow"
onClick=
{
handleSaveSuggestTagList
}
>
Save all
</
button
>
</>
)
}
</
div
>
</>
);
};
function
showCreateTagDialog
()
{
generateDialog
(
{
className
:
"create-tag-dialog"
,
dialogName
:
"create-tag-dialog"
,
},
CreateTagDialog
);
}
export
default
showCreateTagDialog
;
web/src/components/MemoEditor.tsx
View file @
68a77b6e
...
@@ -3,7 +3,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
...
@@ -3,7 +3,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
import
{
useTranslation
}
from
"react-i18next"
;
import
{
useTranslation
}
from
"react-i18next"
;
import
{
deleteMemoResource
,
upsertMemoResource
}
from
"../helpers/api"
;
import
{
deleteMemoResource
,
upsertMemoResource
}
from
"../helpers/api"
;
import
{
TAB_SPACE_WIDTH
,
UNKNOWN_ID
,
VISIBILITY_SELECTOR_ITEMS
}
from
"../helpers/consts"
;
import
{
TAB_SPACE_WIDTH
,
UNKNOWN_ID
,
VISIBILITY_SELECTOR_ITEMS
}
from
"../helpers/consts"
;
import
{
useEditorStore
,
useLocationStore
,
useMemoStore
,
useResourceStore
,
useUserStore
}
from
"../store/module"
;
import
{
useEditorStore
,
useLocationStore
,
useMemoStore
,
useResourceStore
,
use
TagStore
,
use
UserStore
}
from
"../store/module"
;
import
*
as
storage
from
"../helpers/storage"
;
import
*
as
storage
from
"../helpers/storage"
;
import
Icon
from
"./Icon"
;
import
Icon
from
"./Icon"
;
import
toastHelper
from
"./Toast"
;
import
toastHelper
from
"./Toast"
;
...
@@ -44,6 +44,7 @@ const MemoEditor = () => {
...
@@ -44,6 +44,7 @@ const MemoEditor = () => {
const
editorStore
=
useEditorStore
();
const
editorStore
=
useEditorStore
();
const
locationStore
=
useLocationStore
();
const
locationStore
=
useLocationStore
();
const
memoStore
=
useMemoStore
();
const
memoStore
=
useMemoStore
();
const
tagStore
=
useTagStore
();
const
resourceStore
=
useResourceStore
();
const
resourceStore
=
useResourceStore
();
const
[
state
,
setState
]
=
useState
<
State
>
({
const
[
state
,
setState
]
=
useState
<
State
>
({
...
@@ -57,7 +58,7 @@ const MemoEditor = () => {
...
@@ -57,7 +58,7 @@ const MemoEditor = () => {
const
tagSelectorRef
=
useRef
<
HTMLDivElement
>
(
null
);
const
tagSelectorRef
=
useRef
<
HTMLDivElement
>
(
null
);
const
user
=
userStore
.
state
.
user
as
User
;
const
user
=
userStore
.
state
.
user
as
User
;
const
setting
=
user
.
setting
;
const
setting
=
user
.
setting
;
const
tags
=
memo
Store
.
state
.
tags
;
const
tags
=
tag
Store
.
state
.
tags
;
const
memoVisibilityOptionSelectorItems
=
VISIBILITY_SELECTOR_ITEMS
.
map
((
item
)
=>
{
const
memoVisibilityOptionSelectorItems
=
VISIBILITY_SELECTOR_ITEMS
.
map
((
item
)
=>
{
return
{
return
{
value
:
item
.
value
,
value
:
item
.
value
,
...
...
web/src/components/TagList.tsx
View file @
68a77b6e
import
{
useEffect
,
useState
}
from
"react"
;
import
{
useEffect
,
useState
}
from
"react"
;
import
{
useTranslation
}
from
"react-i18next"
;
import
{
useTranslation
}
from
"react-i18next"
;
import
{
useLocationStore
,
use
Memo
Store
,
useUserStore
}
from
"../store/module"
;
import
{
useLocationStore
,
use
Tag
Store
,
useUserStore
}
from
"../store/module"
;
import
useToggle
from
"../hooks/useToggle"
;
import
useToggle
from
"../hooks/useToggle"
;
import
Icon
from
"./Icon"
;
import
Icon
from
"./Icon"
;
import
showCreateTagDialog
from
"./CreateTagDialog"
;
import
"../less/tag-list.less"
;
import
"../less/tag-list.less"
;
interface
Tag
{
interface
Tag
{
...
@@ -15,16 +16,14 @@ const TagList = () => {
...
@@ -15,16 +16,14 @@ const TagList = () => {
const
{
t
}
=
useTranslation
();
const
{
t
}
=
useTranslation
();
const
locationStore
=
useLocationStore
();
const
locationStore
=
useLocationStore
();
const
userStore
=
useUserStore
();
const
userStore
=
useUserStore
();
const
memoStore
=
useMemo
Store
();
const
tagStore
=
useTag
Store
();
const
{
memos
,
tags
:
tagsText
}
=
memoStore
.
state
;
const
tagsText
=
tagStore
.
state
.
tags
;
const
query
=
locationStore
.
state
.
query
;
const
query
=
locationStore
.
state
.
query
;
const
[
tags
,
setTags
]
=
useState
<
Tag
[]
>
([]);
const
[
tags
,
setTags
]
=
useState
<
Tag
[]
>
([]);
useEffect
(()
=>
{
useEffect
(()
=>
{
if
(
memos
.
length
>
0
)
{
tagStore
.
fetchTags
();
memoStore
.
updateTagsState
();
},
[]);
}
},
[
memos
]);
useEffect
(()
=>
{
useEffect
(()
=>
{
const
sortedTags
=
Array
.
from
(
tagsText
).
sort
();
const
sortedTags
=
Array
.
from
(
tagsText
).
sort
();
...
@@ -72,7 +71,15 @@ const TagList = () => {
...
@@ -72,7 +71,15 @@ const TagList = () => {
return
(
return
(
<
div
className=
"tags-wrapper"
>
<
div
className=
"tags-wrapper"
>
<
p
className=
"title-text"
>
{
t
(
"common.tags"
)
}
</
p
>
<
div
className=
"w-full flex flex-row justify-start items-center px-4 mb-1"
>
<
span
className=
"text-sm leading-6 font-mono text-gray-400"
>
{
t
(
"common.tags"
)
}
</
span
>
<
button
onClick=
{
()
=>
showCreateTagDialog
()
}
className=
"flex flex-col justify-center items-center w-5 h-5 bg-gray-200 dark:bg-zinc-700 rounded ml-2 hover:shadow"
>
<
Icon
.
Plus
className=
"w-4 h-4 text-gray-400"
/>
</
button
>
</
div
>
<
div
className=
"tags-container"
>
<
div
className=
"tags-container"
>
{
tags
.
map
((
t
,
idx
)
=>
(
{
tags
.
map
((
t
,
idx
)
=>
(
<
TagItemContainer
key=
{
t
.
text
+
"-"
+
idx
}
tag=
{
t
}
tagQuery=
{
query
?.
tag
}
/>
<
TagItemContainer
key=
{
t
.
text
+
"-"
+
idx
}
tag=
{
t
}
tagQuery=
{
query
?.
tag
}
/>
...
...
web/src/components/UserBanner.tsx
View file @
68a77b6e
import
{
useCallback
,
useEffect
,
useState
}
from
"react"
;
import
{
useCallback
,
useEffect
,
useState
}
from
"react"
;
import
{
useTranslation
}
from
"react-i18next"
;
import
{
useTranslation
}
from
"react-i18next"
;
import
{
useNavigate
}
from
"react-router-dom"
;
import
{
useNavigate
}
from
"react-router-dom"
;
import
{
useLocationStore
,
useMemoStore
,
useUserStore
}
from
"../store/module"
;
import
{
useLocationStore
,
useMemoStore
,
use
TagStore
,
use
UserStore
}
from
"../store/module"
;
import
{
getMemoStats
}
from
"../helpers/api"
;
import
{
getMemoStats
}
from
"../helpers/api"
;
import
*
as
utils
from
"../helpers/utils"
;
import
*
as
utils
from
"../helpers/utils"
;
import
Icon
from
"./Icon"
;
import
Icon
from
"./Icon"
;
...
@@ -17,8 +17,10 @@ const UserBanner = () => {
...
@@ -17,8 +17,10 @@ const UserBanner = () => {
const
locationStore
=
useLocationStore
();
const
locationStore
=
useLocationStore
();
const
userStore
=
useUserStore
();
const
userStore
=
useUserStore
();
const
memoStore
=
useMemoStore
();
const
memoStore
=
useMemoStore
();
const
tagStore
=
useTagStore
();
const
{
user
,
owner
}
=
userStore
.
state
;
const
{
user
,
owner
}
=
userStore
.
state
;
const
{
memos
,
tags
}
=
memoStore
.
state
;
const
{
memos
}
=
memoStore
.
state
;
const
tags
=
tagStore
.
state
.
tags
;
const
[
username
,
setUsername
]
=
useState
(
"Memos"
);
const
[
username
,
setUsername
]
=
useState
(
"Memos"
);
const
[
memoAmount
,
setMemoAmount
]
=
useState
(
0
);
const
[
memoAmount
,
setMemoAmount
]
=
useState
(
0
);
const
[
createdDays
,
setCreatedDays
]
=
useState
(
0
);
const
[
createdDays
,
setCreatedDays
]
=
useState
(
0
);
...
...
web/src/helpers/api.ts
View file @
68a77b6e
...
@@ -187,6 +187,20 @@ export function getTagList(tagFind?: TagFind) {
...
@@ -187,6 +187,20 @@ export function getTagList(tagFind?: TagFind) {
return
axios
.
get
<
ResponseObject
<
string
[]
>>
(
`/api/tag?
${
queryList
.
join
(
"&"
)}
`
);
return
axios
.
get
<
ResponseObject
<
string
[]
>>
(
`/api/tag?
${
queryList
.
join
(
"&"
)}
`
);
}
}
export
function
getTagSuggestionList
()
{
return
axios
.
get
<
ResponseObject
<
string
[]
>>
(
`/api/tag/suggestion`
);
}
export
function
upsertTag
(
tagName
:
string
)
{
return
axios
.
post
<
ResponseObject
<
string
>>
(
`/api/tag`
,
{
name
:
tagName
,
});
}
export
function
deleteTag
(
tagName
:
string
)
{
return
axios
.
delete
<
ResponseObject
<
string
>>
(
`/api/tag/
${
tagName
}
`
);
}
export
async
function
getRepoStarCount
()
{
export
async
function
getRepoStarCount
()
{
const
{
data
}
=
await
axios
.
get
(
`https://api.github.com/repos/usememos/memos`
,
{
const
{
data
}
=
await
axios
.
get
(
`https://api.github.com/repos/usememos/memos`
,
{
headers
:
{
headers
:
{
...
...
web/src/less/tag-list.less
View file @
68a77b6e
.tags-wrapper {
.tags-wrapper {
@apply flex flex-col justify-start items-start px-2 w-full h-auto flex-nowrap pb-4 mt-2 grow hide-scrollbar;
@apply flex flex-col justify-start items-start px-2 w-full h-auto flex-nowrap pb-4 mt-2 grow hide-scrollbar;
> .title-text {
@apply w-full px-4 text-sm leading-6 font-mono text-gray-400;
}
> .tags-container {
> .tags-container {
@apply flex flex-col justify-start items-start relative w-full h-auto flex-nowrap mb-2 mt-1;
@apply flex flex-col justify-start items-start relative w-full h-auto flex-nowrap mb-2 mt-1;
...
...
web/src/store/index.ts
View file @
68a77b6e
...
@@ -8,12 +8,14 @@ import shortcutReducer from "./reducer/shortcut";
...
@@ -8,12 +8,14 @@ import shortcutReducer from "./reducer/shortcut";
import
locationReducer
from
"./reducer/location"
;
import
locationReducer
from
"./reducer/location"
;
import
resourceReducer
from
"./reducer/resource"
;
import
resourceReducer
from
"./reducer/resource"
;
import
dialogReducer
from
"./reducer/dialog"
;
import
dialogReducer
from
"./reducer/dialog"
;
import
tagReducer
from
"./reducer/tag"
;
const
store
=
configureStore
({
const
store
=
configureStore
({
reducer
:
{
reducer
:
{
global
:
globalReducer
,
global
:
globalReducer
,
user
:
userReducer
,
user
:
userReducer
,
memo
:
memoReducer
,
memo
:
memoReducer
,
tag
:
tagReducer
,
editor
:
editorReducer
,
editor
:
editorReducer
,
shortcut
:
shortcutReducer
,
shortcut
:
shortcutReducer
,
location
:
locationReducer
,
location
:
locationReducer
,
...
...
web/src/store/module/index.ts
View file @
68a77b6e
...
@@ -2,6 +2,7 @@ export * from "./editor";
...
@@ -2,6 +2,7 @@ export * from "./editor";
export
*
from
"./global"
;
export
*
from
"./global"
;
export
*
from
"./location"
;
export
*
from
"./location"
;
export
*
from
"./memo"
;
export
*
from
"./memo"
;
export
*
from
"./tag"
;
export
*
from
"./resource"
;
export
*
from
"./resource"
;
export
*
from
"./shortcut"
;
export
*
from
"./shortcut"
;
export
*
from
"./user"
;
export
*
from
"./user"
;
...
...
web/src/store/module/memo.ts
View file @
68a77b6e
...
@@ -3,7 +3,7 @@ import * as api from "../../helpers/api";
...
@@ -3,7 +3,7 @@ import * as api from "../../helpers/api";
import
{
DEFAULT_MEMO_LIMIT
}
from
"../../helpers/consts"
;
import
{
DEFAULT_MEMO_LIMIT
}
from
"../../helpers/consts"
;
import
{
useUserStore
}
from
"./"
;
import
{
useUserStore
}
from
"./"
;
import
store
,
{
useAppSelector
}
from
"../"
;
import
store
,
{
useAppSelector
}
from
"../"
;
import
{
createMemo
,
deleteMemo
,
patchMemo
,
setIsFetching
,
setMemos
,
setTags
}
from
"../reducer/memo"
;
import
{
createMemo
,
deleteMemo
,
patchMemo
,
setIsFetching
,
setMemos
}
from
"../reducer/memo"
;
const
convertResponseModelMemo
=
(
memo
:
Memo
):
Memo
=>
{
const
convertResponseModelMemo
=
(
memo
:
Memo
):
Memo
=>
{
return
{
return
{
...
@@ -85,14 +85,6 @@ export const useMemoStore = () => {
...
@@ -85,14 +85,6 @@ export const useMemoStore = () => {
return
await
fetchMemoById
(
memoId
);
return
await
fetchMemoById
(
memoId
);
},
},
updateTagsState
:
async
()
=>
{
const
tagFind
:
TagFind
=
{};
if
(
userStore
.
isVisitorMode
())
{
tagFind
.
creatorId
=
userStore
.
getUserIdFromPath
();
}
const
{
data
}
=
(
await
api
.
getTagList
(
tagFind
)).
data
;
store
.
dispatch
(
setTags
(
data
));
},
getLinkedMemos
:
async
(
memoId
:
MemoId
):
Promise
<
Memo
[]
>
=>
{
getLinkedMemos
:
async
(
memoId
:
MemoId
):
Promise
<
Memo
[]
>
=>
{
const
regex
=
new
RegExp
(
`[@(.+?)](
${
memoId
}
)`
);
const
regex
=
new
RegExp
(
`[@(.+?)](
${
memoId
}
)`
);
return
state
.
memos
.
filter
((
m
)
=>
m
.
content
.
match
(
regex
));
return
state
.
memos
.
filter
((
m
)
=>
m
.
content
.
match
(
regex
));
...
...
web/src/store/module/tag.ts
0 → 100644
View file @
68a77b6e
import
store
,
{
useAppSelector
}
from
".."
;
import
*
as
api
from
"../../helpers/api"
;
import
{
deleteTag
,
setTags
,
upsertTag
}
from
"../reducer/tag"
;
import
{
useUserStore
}
from
"./"
;
export
const
useTagStore
=
()
=>
{
const
state
=
useAppSelector
((
state
)
=>
state
.
tag
);
const
userStore
=
useUserStore
();
return
{
state
,
getState
:
()
=>
{
return
store
.
getState
().
tag
;
},
fetchTags
:
async
()
=>
{
const
tagFind
:
TagFind
=
{};
if
(
userStore
.
isVisitorMode
())
{
tagFind
.
creatorId
=
userStore
.
getUserIdFromPath
();
}
const
{
data
}
=
(
await
api
.
getTagList
(
tagFind
)).
data
;
store
.
dispatch
(
setTags
(
data
));
},
upsertTag
:
async
(
tagName
:
string
)
=>
{
await
api
.
upsertTag
(
tagName
);
store
.
dispatch
(
upsertTag
(
tagName
));
},
deleteTag
:
async
(
tagName
:
string
)
=>
{
await
api
.
deleteTag
(
tagName
);
store
.
dispatch
(
deleteTag
(
tagName
));
},
};
};
web/src/store/reducer/memo.ts
View file @
68a77b6e
...
@@ -2,7 +2,6 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit";
...
@@ -2,7 +2,6 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit";
interface
State
{
interface
State
{
memos
:
Memo
[];
memos
:
Memo
[];
tags
:
string
[];
isFetching
:
boolean
;
isFetching
:
boolean
;
}
}
...
@@ -10,7 +9,6 @@ const memoSlice = createSlice({
...
@@ -10,7 +9,6 @@ const memoSlice = createSlice({
name
:
"memo"
,
name
:
"memo"
,
initialState
:
{
initialState
:
{
memos
:
[],
memos
:
[],
tags
:
[],
// isFetching flag should starts with true.
// isFetching flag should starts with true.
isFetching
:
true
,
isFetching
:
true
,
}
as
State
,
}
as
State
,
...
@@ -52,12 +50,6 @@ const memoSlice = createSlice({
...
@@ -52,12 +50,6 @@ const memoSlice = createSlice({
}),
}),
};
};
},
},
setTags
:
(
state
,
action
:
PayloadAction
<
string
[]
>
)
=>
{
return
{
...
state
,
tags
:
action
.
payload
,
};
},
setIsFetching
:
(
state
,
action
:
PayloadAction
<
boolean
>
)
=>
{
setIsFetching
:
(
state
,
action
:
PayloadAction
<
boolean
>
)
=>
{
return
{
return
{
...
state
,
...
state
,
...
@@ -67,6 +59,6 @@ const memoSlice = createSlice({
...
@@ -67,6 +59,6 @@ const memoSlice = createSlice({
},
},
});
});
export
const
{
setMemos
,
createMemo
,
patchMemo
,
deleteMemo
,
set
Tags
,
set
IsFetching
}
=
memoSlice
.
actions
;
export
const
{
setMemos
,
createMemo
,
patchMemo
,
deleteMemo
,
setIsFetching
}
=
memoSlice
.
actions
;
export
default
memoSlice
.
reducer
;
export
default
memoSlice
.
reducer
;
web/src/store/reducer/tag.ts
0 → 100644
View file @
68a77b6e
import
{
createSlice
,
PayloadAction
}
from
"@reduxjs/toolkit"
;
interface
State
{
tags
:
string
[];
}
const
tagSlice
=
createSlice
({
name
:
"tag"
,
initialState
:
{
tags
:
[],
}
as
State
,
reducers
:
{
setTags
:
(
state
,
action
:
PayloadAction
<
string
[]
>
)
=>
{
return
{
...
state
,
tags
:
action
.
payload
,
};
},
upsertTag
:
(
state
,
action
:
PayloadAction
<
string
>
)
=>
{
if
(
state
.
tags
.
includes
(
action
.
payload
))
{
return
state
;
}
return
{
...
state
,
tags
:
state
.
tags
.
concat
(
action
.
payload
),
};
},
deleteTag
:
(
state
,
action
:
PayloadAction
<
string
>
)
=>
{
return
{
...
state
,
tags
:
state
.
tags
.
filter
((
tag
)
=>
{
return
tag
!==
action
.
payload
;
}),
};
},
},
});
export
const
{
setTags
,
upsertTag
,
deleteTag
}
=
tagSlice
.
actions
;
export
default
tagSlice
.
reducer
;
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