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
2db86f66
Commit
2db86f66
authored
Feb 03, 2025
by
johnnyjoy
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: implement shortcut components
parent
3a085f36
Changes
8
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
238 additions
and
5 deletions
+238
-5
CreateShortcutDialog.tsx
web/src/components/CreateShortcutDialog.tsx
+135
-0
HomeSidebar.tsx
web/src/components/HomeSidebar/HomeSidebar.tsx
+2
-0
ShortcutsSection.tsx
web/src/components/HomeSidebar/ShortcutsSection.tsx
+76
-0
PagedMemoList.tsx
web/src/components/PagedMemoList/PagedMemoList.tsx
+3
-1
en.json
web/src/locales/en.json
+4
-2
Home.tsx
web/src/pages/Home.tsx
+4
-1
memoFilter.ts
web/src/store/v1/memoFilter.ts
+3
-0
user.ts
web/src/store/v1/user.ts
+11
-1
No files found.
web/src/components/CreateShortcutDialog.tsx
0 → 100644
View file @
2db86f66
import
{
Input
,
Textarea
}
from
"@mui/joy"
;
import
{
Button
}
from
"@usememos/mui"
;
import
{
XIcon
}
from
"lucide-react"
;
import
React
,
{
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
{
userServiceClient
}
from
"@/grpcweb"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
useLoading
from
"@/hooks/useLoading"
;
import
{
useUserStore
}
from
"@/store/v1"
;
import
{
Shortcut
}
from
"@/types/proto/api/v1/user_service"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
generateUUID
}
from
"@/utils/uuid"
;
import
{
generateDialog
}
from
"./Dialog"
;
interface
Props
extends
DialogProps
{
shortcut
?:
Shortcut
;
}
const
CreateShortcutDialog
:
React
.
FC
<
Props
>
=
(
props
:
Props
)
=>
{
const
{
destroy
}
=
props
;
const
t
=
useTranslate
();
const
user
=
useCurrentUser
();
const
userStore
=
useUserStore
();
const
[
shortcut
,
setShortcut
]
=
useState
(
Shortcut
.
fromPartial
({
...
props
.
shortcut
}));
const
requestState
=
useLoading
(
false
);
const
isCreating
=
!
props
.
shortcut
;
const
onShortcutTitleChange
=
(
e
:
React
.
ChangeEvent
<
HTMLInputElement
>
)
=>
{
setShortcut
({
...
shortcut
,
title
:
e
.
target
.
value
});
};
const
onShortcutFilterChange
=
(
e
:
React
.
ChangeEvent
<
HTMLTextAreaElement
>
)
=>
{
setShortcut
({
...
shortcut
,
filter
:
e
.
target
.
value
});
};
const
handleConfirm
=
async
()
=>
{
if
(
!
shortcut
.
title
||
!
shortcut
.
filter
)
{
toast
.
error
(
"Title and filter cannot be empty"
);
return
;
}
try
{
if
(
isCreating
)
{
await
userServiceClient
.
createShortcut
({
parent
:
user
.
name
,
shortcut
:
{
...
shortcut
,
id
:
generateUUID
(),
},
});
toast
.
success
(
"Create shortcut successfully"
);
}
else
{
await
userServiceClient
.
updateShortcut
({
parent
:
user
.
name
,
shortcut
,
updateMask
:
[
"title"
,
"filter"
]
});
toast
.
success
(
"Update shortcut successfully"
);
}
// Refresh shortcuts.
await
userStore
.
fetchShortcuts
();
destroy
();
}
catch
(
error
:
any
)
{
console
.
error
(
error
);
toast
.
error
(
error
.
details
);
}
};
return
(
<>
<
div
className=
"dialog-header-container"
>
<
p
className=
"title-text"
>
{
`${isCreating ? "Create" : "Edit"} Shortcut`
}
</
p
>
<
Button
size=
"sm"
variant=
"plain"
onClick=
{
()
=>
destroy
()
}
>
<
XIcon
className=
"w-5 h-auto"
/>
</
Button
>
</
div
>
<
div
className=
"dialog-content-container max-w-md min-w-72"
>
<
div
className=
"w-full flex flex-col justify-start items-start mb-3"
>
<
span
className=
"text-sm whitespace-nowrap mb-1"
>
Title
</
span
>
<
Input
className=
"w-full"
type=
"text"
placeholder=
""
value=
{
shortcut
.
title
}
onChange=
{
onShortcutTitleChange
}
/>
<
span
className=
"text-sm whitespace-nowrap mt-3 mb-1"
>
Filter
</
span
>
<
Textarea
className=
"w-full"
minRows=
{
3
}
maxRows=
{
5
}
size=
"sm"
placeholder=
{
"Shortcut filter"
}
value=
{
shortcut
.
filter
}
onChange=
{
onShortcutFilterChange
}
/>
</
div
>
<
div
className=
"w-full opacity-70"
>
<
p
className=
"text-sm"
>
{
t
(
"common.learn-more"
)
}
:
</
p
>
<
ul
className=
"list-disc list-inside text-sm pl-2 mt-1"
>
<
li
>
<
a
className=
"text-sm text-blue-600 hover:underline"
href=
"https://www.usememos.com/docs/getting-started/shortcuts"
target=
"_blank"
>
Docs - Shortcuts
</
a
>
</
li
>
<
li
>
<
a
className=
"text-sm text-blue-600 hover:underline"
href=
"https://www.usememos.com/docs/getting-started/shortcuts#how-to-write-a-filter-in-a-shortcut"
target=
"_blank"
>
How to Write a Filter in a Shortcut?
</
a
>
</
li
>
</
ul
>
</
div
>
<
div
className=
"w-full flex flex-row justify-end items-center space-x-2 mt-2"
>
<
Button
variant=
"plain"
disabled=
{
requestState
.
isLoading
}
onClick=
{
destroy
}
>
{
t
(
"common.cancel"
)
}
</
Button
>
<
Button
color=
"primary"
disabled=
{
requestState
.
isLoading
}
onClick=
{
handleConfirm
}
>
{
t
(
"common.confirm"
)
}
</
Button
>
</
div
>
</
div
>
</>
);
};
function
showCreateShortcutDialog
(
props
:
Pick
<
Props
,
"shortcut"
>
)
{
generateDialog
(
{
className
:
"create-shortcut-dialog"
,
dialogName
:
"create-shortcut-dialog"
,
},
CreateShortcutDialog
,
props
,
);
}
export
default
showCreateShortcutDialog
;
web/src/components/HomeSidebar/HomeSidebar.tsx
View file @
2db86f66
...
...
@@ -4,6 +4,7 @@ import StatisticsView from "@/components/StatisticsView";
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
{
useMemoList
,
useUserStatsStore
}
from
"@/store/v1"
;
import
{
cn
}
from
"@/utils"
;
import
ShortcutsSection
from
"./ShortcutsSection"
;
import
TagsSection
from
"./TagsSection"
;
interface
Props
{
...
...
@@ -32,6 +33,7 @@ const HomeSidebar = (props: Props) => {
>
<
SearchBar
/>
<
StatisticsView
/>
<
ShortcutsSection
/>
<
TagsSection
/>
</
aside
>
);
...
...
web/src/components/HomeSidebar/ShortcutsSection.tsx
0 → 100644
View file @
2db86f66
import
{
Dropdown
,
Menu
,
MenuButton
,
MenuItem
,
Tooltip
}
from
"@mui/joy"
;
import
{
Edit3Icon
,
MoreVerticalIcon
,
TrashIcon
,
PlusIcon
}
from
"lucide-react"
;
import
{
userServiceClient
}
from
"@/grpcweb"
;
import
useAsyncEffect
from
"@/hooks/useAsyncEffect"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
{
useMemoFilterStore
,
useUserStore
}
from
"@/store/v1"
;
import
{
Shortcut
}
from
"@/types/proto/api/v1/user_service"
;
import
{
cn
}
from
"@/utils"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
showCreateShortcutDialog
from
"../CreateShortcutDialog"
;
const
ShortcutsSection
=
()
=>
{
const
t
=
useTranslate
();
const
user
=
useCurrentUser
();
const
userStore
=
useUserStore
();
const
memoFilterStore
=
useMemoFilterStore
();
const
shortcuts
=
userStore
.
getState
().
shortcuts
;
useAsyncEffect
(
async
()
=>
{
await
userStore
.
fetchShortcuts
();
},
[]);
const
handleDeleteShortcut
=
async
(
shortcut
:
Shortcut
)
=>
{
const
confirmed
=
window
.
confirm
(
"Are you sure you want to delete this shortcut?"
);
if
(
confirmed
)
{
await
userServiceClient
.
deleteShortcut
({
parent
:
user
.
name
,
id
:
shortcut
.
id
});
await
userStore
.
fetchShortcuts
();
}
};
return
(
<
div
className=
"w-full flex flex-col justify-start items-start mt-3 px-1 h-auto shrink-0 flex-nowrap hide-scrollbar"
>
<
div
className=
"flex flex-row justify-between items-center w-full gap-1 mb-1 text-sm leading-6 text-gray-400 select-none"
>
<
span
>
{
t
(
"common.shortcuts"
)
}
</
span
>
<
Tooltip
title=
{
t
(
"common.create"
)
}
placement=
"top"
>
<
PlusIcon
className=
"w-4 h-auto"
onClick=
{
()
=>
showCreateShortcutDialog
({})
}
/>
</
Tooltip
>
</
div
>
<
div
className=
"w-full flex flex-row justify-start items-center relative flex-wrap gap-x-2 gap-y-1"
>
{
shortcuts
.
map
((
shortcut
)
=>
{
const
selected
=
memoFilterStore
.
shortcut
===
shortcut
.
id
;
return
(
<
div
key=
{
shortcut
.
id
}
className=
"shrink-0 w-full text-sm rounded-md leading-6 flex flex-row justify-between items-center select-none gap-2 text-gray-600 dark:text-gray-400 dark:border-zinc-800"
>
<
span
className=
{
cn
(
"truncate cursor-pointer dark:opacity-80"
,
selected
&&
"font-medium underline"
)
}
onClick=
{
()
=>
(
selected
?
memoFilterStore
.
setShortcut
(
undefined
)
:
memoFilterStore
.
setShortcut
(
shortcut
.
id
))
}
>
{
shortcut
.
title
}
</
span
>
<
Dropdown
>
<
MenuButton
slots=
{
{
root
:
"div"
}
}
>
<
MoreVerticalIcon
className=
"w-4 h-auto shrink-0 opacity-40"
/>
</
MenuButton
>
<
Menu
size=
"sm"
placement=
"bottom-start"
>
<
MenuItem
onClick=
{
()
=>
showCreateShortcutDialog
({
shortcut
})
}
>
<
Edit3Icon
className=
"w-4 h-auto"
/>
{
t
(
"common.edit"
)
}
</
MenuItem
>
<
MenuItem
color=
"danger"
onClick=
{
()
=>
handleDeleteShortcut
(
shortcut
)
}
>
<
TrashIcon
className=
"w-4 h-auto"
/>
{
t
(
"common.delete"
)
}
</
MenuItem
>
</
Menu
>
</
Dropdown
>
</
div
>
);
})
}
</
div
>
</
div
>
);
};
export
default
ShortcutsSection
;
web/src/components/PagedMemoList/PagedMemoList.tsx
View file @
2db86f66
...
...
@@ -16,6 +16,7 @@ interface Props {
owner
?:
string
;
state
?:
State
;
direction
?:
Direction
;
filter
?:
string
;
oldFilter
?:
string
;
pageSize
?:
number
;
}
...
...
@@ -42,6 +43,7 @@ const PagedMemoList = (props: Props) => {
parent
:
props
.
owner
||
""
,
state
:
props
.
state
||
State
.
NORMAL
,
direction
:
props
.
direction
||
Direction
.
DESC
,
filter
:
props
.
filter
||
""
,
oldFilter
:
props
.
oldFilter
||
""
,
pageSize
:
props
.
pageSize
||
DEFAULT_LIST_MEMOS_PAGE_SIZE
,
pageToken
:
nextPageToken
,
...
...
@@ -60,7 +62,7 @@ const PagedMemoList = (props: Props) => {
useEffect
(()
=>
{
refreshList
();
},
[
props
.
owner
,
props
.
state
,
props
.
direction
,
props
.
oldFilter
,
props
.
pageSize
]);
},
[
props
.
owner
,
props
.
state
,
props
.
direction
,
props
.
filter
,
props
.
oldFilter
,
props
.
pageSize
]);
const
children
=
(
<
div
className=
"flex flex-col justify-start items-start w-full max-w-full"
>
...
...
web/src/locales/en.json
View file @
2db86f66
...
...
@@ -79,7 +79,8 @@
"username"
:
"Username"
,
"version"
:
"Version"
,
"visibility"
:
"Visibility"
,
"yourself"
:
"Yourself"
"yourself"
:
"Yourself"
,
"shortcuts"
:
"Shortcuts"
},
"days"
:
{
"fri"
:
"Fri"
,
...
...
@@ -371,6 +372,7 @@
},
"version"
:
"Version"
},
"shortcut"
:
{},
"tag"
:
{
"all-tags"
:
"All Tags"
,
"create-tag"
:
"Create Tag"
,
...
...
@@ -391,4 +393,4 @@
"blogs"
:
"Blogs"
,
"documents"
:
"Documents"
}
}
\ No newline at end of file
}
web/src/pages/Home.tsx
View file @
2db86f66
...
...
@@ -8,7 +8,7 @@ import MobileHeader from "@/components/MobileHeader";
import
PagedMemoList
from
"@/components/PagedMemoList"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
useResponsiveWidth
from
"@/hooks/useResponsiveWidth"
;
import
{
useMemoFilterStore
}
from
"@/store/v1"
;
import
{
useMemoFilterStore
,
useUserStore
}
from
"@/store/v1"
;
import
{
Direction
,
State
}
from
"@/types/proto/api/v1/common"
;
import
{
Memo
}
from
"@/types/proto/api/v1/memo_service"
;
import
{
cn
}
from
"@/utils"
;
...
...
@@ -16,7 +16,9 @@ import { cn } from "@/utils";
const
Home
=
()
=>
{
const
{
md
}
=
useResponsiveWidth
();
const
user
=
useCurrentUser
();
const
userStore
=
useUserStore
();
const
memoFilterStore
=
useMemoFilterStore
();
const
selectedShortcut
=
userStore
.
shortcuts
.
find
((
shortcut
)
=>
shortcut
.
id
===
memoFilterStore
.
shortcut
);
const
memoListFilter
=
useMemo
(()
=>
{
const
conditions
=
[];
...
...
@@ -79,6 +81,7 @@ const Home = () => {
}
owner=
{
user
.
name
}
direction=
{
memoFilterStore
.
orderByTimeAsc
?
Direction
.
ASC
:
Direction
.
DESC
}
filter=
{
selectedShortcut
?.
filter
||
""
}
oldFilter=
{
memoListFilter
}
/>
</
div
>
...
...
web/src/store/v1/memoFilter.ts
View file @
2db86f66
...
...
@@ -41,6 +41,8 @@ export const stringifyFilters = (filters: MemoFilter[]): string => {
interface
State
{
filters
:
MemoFilter
[];
orderByTimeAsc
:
boolean
;
// The id of selected shortcut.
shortcut
?:
string
;
}
const
getInitialState
=
():
State
=>
{
...
...
@@ -59,5 +61,6 @@ export const useMemoFilterStore = create(
addFilter
:
(
filter
:
MemoFilter
)
=>
set
((
state
)
=>
({
filters
:
uniqBy
([...
state
.
filters
,
filter
],
getMemoFilterKey
)
})),
removeFilter
:
(
filterFn
:
(
f
:
MemoFilter
)
=>
boolean
)
=>
set
((
state
)
=>
({
filters
:
state
.
filters
.
filter
((
f
)
=>
!
filterFn
(
f
))
})),
setOrderByTimeAsc
:
(
orderByTimeAsc
:
boolean
)
=>
set
({
orderByTimeAsc
}),
setShortcut
:
(
shortcut
?:
string
)
=>
set
({
shortcut
}),
})),
);
web/src/store/v1/user.ts
View file @
2db86f66
import
{
create
}
from
"zustand"
;
import
{
combine
}
from
"zustand/middleware"
;
import
{
authServiceClient
,
userServiceClient
}
from
"@/grpcweb"
;
import
{
User
,
UserSetting
,
User_Role
}
from
"@/types/proto/api/v1/user_service"
;
import
{
Shortcut
,
User
,
UserSetting
,
User_Role
}
from
"@/types/proto/api/v1/user_service"
;
interface
State
{
userMapByName
:
Record
<
string
,
User
>
;
// The name of current user. Format: `users/${uid}`
currentUser
?:
string
;
userSetting
?:
UserSetting
;
shortcuts
:
Shortcut
[];
}
const
getDefaultState
=
():
State
=>
({
userMapByName
:
{},
currentUser
:
undefined
,
userSetting
:
undefined
,
shortcuts
:
[],
});
const
getDefaultUserSetting
=
()
=>
{
...
...
@@ -129,6 +131,14 @@ export const useUserStore = create(
set
({
userSetting
:
updatedUserSetting
});
return
updatedUserSetting
;
},
fetchShortcuts
:
async
()
=>
{
const
{
currentUser
}
=
get
();
if
(
!
currentUser
)
{
return
;
}
const
{
shortcuts
}
=
await
userServiceClient
.
listShortcuts
({
parent
:
currentUser
});
set
({
shortcuts
});
},
})),
);
...
...
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