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
240d89fb
Commit
240d89fb
authored
Jul 06, 2025
by
Johnny
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: migrate dialogs
parent
f7013853
Changes
33
Hide whitespace changes
Inline
Side-by-side
Showing
33 changed files
with
1196 additions
and
1200 deletions
+1196
-1200
attachment_service.proto
proto/api/v1/attachment_service.proto
+1
-1
attachment_service.pb.go
proto/gen/api/v1/attachment_service.pb.go
+3
-3
AttachmentIcon.tsx
web/src/components/AttachmentIcon.tsx
+38
-12
ChangeMemberPasswordDialog.tsx
web/src/components/ChangeMemberPasswordDialog.tsx
+50
-53
CreateAccessTokenDialog.tsx
web/src/components/CreateAccessTokenDialog.tsx
+32
-47
CreateIdentityProviderDialog.tsx
web/src/components/CreateIdentityProviderDialog.tsx
+188
-202
CreateShortcutDialog.tsx
web/src/components/CreateShortcutDialog.tsx
+69
-71
CreateUserDialog.tsx
web/src/components/CreateUserDialog.tsx
+74
-81
CreateWebhookDialog.tsx
web/src/components/CreateWebhookDialog.tsx
+41
-53
BaseDialog.tsx
web/src/components/Dialog/BaseDialog.tsx
+0
-99
index.ts
web/src/components/Dialog/index.ts
+0
-1
ShortcutsSection.tsx
web/src/components/HomeSidebar/ShortcutsSection.tsx
+27
-3
TagsSection.tsx
web/src/components/HomeSidebar/TagsSection.tsx
+24
-2
MemoAttachmentListView.tsx
web/src/components/MemoAttachmentListView.tsx
+23
-3
MemoView.tsx
web/src/components/MemoView.tsx
+14
-2
PreviewImageDialog.tsx
web/src/components/PreviewImageDialog.tsx
+70
-180
RenameTagDialog.tsx
web/src/components/RenameTagDialog.tsx
+35
-43
AccessTokenSection.tsx
web/src/components/Settings/AccessTokenSection.tsx
+15
-7
MemberSection.tsx
web/src/components/Settings/MemberSection.tsx
+25
-116
MyAccountSection.tsx
web/src/components/Settings/MyAccountSection.tsx
+21
-6
SSOSection.tsx
web/src/components/Settings/SSOSection.tsx
+27
-5
WebhookSection.tsx
web/src/components/Settings/WebhookSection.tsx
+9
-7
WorkspaceSection.tsx
web/src/components/Settings/WorkspaceSection.tsx
+13
-2
UpdateAccountDialog.tsx
web/src/components/UpdateAccountDialog.tsx
+73
-70
UpdateCustomizedProfileDialog.new.tsx
web/src/components/UpdateCustomizedProfileDialog.new.tsx
+0
-0
UpdateCustomizedProfileDialog.tsx
web/src/components/UpdateCustomizedProfileDialog.tsx
+75
-56
WorkspaceSection.example.tsx
web/src/components/examples/WorkspaceSection.example.tsx
+0
-0
dialog.tsx
web/src/components/ui/dialog.tsx
+100
-71
useDialog.ts
web/src/hooks/useDialog.ts
+118
-0
Attachments.tsx
web/src/pages/Attachments.tsx
+1
-1
Inboxes.tsx
web/src/pages/Inboxes.tsx
+1
-1
Setting.tsx
web/src/pages/Setting.tsx
+1
-1
attachment_service.ts
web/src/types/proto/api/v1/attachment_service.ts
+28
-1
No files found.
proto/api/v1/attachment_service.proto
View file @
240d89fb
...
...
@@ -34,7 +34,7 @@ service AttachmentService {
// GetAttachmentBinary returns a attachment binary by name.
rpc
GetAttachmentBinary
(
GetAttachmentBinaryRequest
)
returns
(
google.api.HttpBody
)
{
option
(
google.api.http
)
=
{
get
:
"/file/{name=attachments/*}/{filename}"
};
option
(
google.api.method_signature
)
=
"name,filename"
;
option
(
google.api.method_signature
)
=
"name,filename
,thumbnail
"
;
}
// UpdateAttachment updates a attachment.
rpc
UpdateAttachment
(
UpdateAttachmentRequest
)
returns
(
Attachment
)
{
...
...
proto/gen/api/v1/attachment_service.pb.go
View file @
240d89fb
...
...
@@ -596,14 +596,14 @@ const file_api_v1_attachment_service_proto_rawDesc = "" +
"updateMask
\"
N
\n
"
+
"
\x17
DeleteAttachmentRequest
\x12
3
\n
"
+
"
\x04
name
\x18\x01
\x01
(
\t
B
\x1f\xe0
A
\x02\xfa
A
\x19\n
"
+
"
\x17
memos.api.v1/AttachmentR
\x04
name2
\x
db
\x06\n
"
+
"
\x17
memos.api.v1/AttachmentR
\x04
name2
\x
e5
\x06\n
"
+
"
\x11
AttachmentService
\x12\x89\x01\n
"
+
"
\x10
CreateAttachment
\x12
%.memos.api.v1.CreateAttachmentRequest
\x1a\x18
.memos.api.v1.Attachment
\"
4
\xda
A
\n
"
+
"attachment
\x82\xd3\xe4\x93\x02
!:
\n
"
+
"attachment
\"\x13
/api/v1/attachments
\x12
{
\n
"
+
"
\x0f
ListAttachments
\x12
$.memos.api.v1.ListAttachmentsRequest
\x1a
%.memos.api.v1.ListAttachmentsResponse
\"\x1b\x82\xd3\xe4\x93\x02\x15\x12\x13
/api/v1/attachments
\x12
z
\n
"
+
"
\r
GetAttachment
\x12\"
.memos.api.v1.GetAttachmentRequest
\x1a\x18
.memos.api.v1.Attachment
\"
+
\xda
A
\x04
name
\x82\xd3\xe4\x93\x02\x1e\x12\x1c
/api/v1/{name=attachments/*}
\x12\x9
4
\x01\n
"
+
"
\x13
GetAttachmentBinary
\x12
(.memos.api.v1.GetAttachmentBinaryRequest
\x1a\x14
.google.api.HttpBody
\"
=
\xda
A
\r
name,filename
\x82\xd3\xe4\x93\x02
'
\x12
%/file/{name=attachments/*}/{filename}
\x12\xa9\x01\n
"
+
"
\r
GetAttachment
\x12\"
.memos.api.v1.GetAttachmentRequest
\x1a\x18
.memos.api.v1.Attachment
\"
+
\xda
A
\x04
name
\x82\xd3\xe4\x93\x02\x1e\x12\x1c
/api/v1/{name=attachments/*}
\x12\x9
e
\x01\n
"
+
"
\x13
GetAttachmentBinary
\x12
(.memos.api.v1.GetAttachmentBinaryRequest
\x1a\x14
.google.api.HttpBody
\"
G
\xda
A
\x17
name,filename,thumbnail
\x82\xd3\xe4\x93\x02
'
\x12
%/file/{name=attachments/*}/{filename}
\x12\xa9\x01\n
"
+
"
\x10
UpdateAttachment
\x12
%.memos.api.v1.UpdateAttachmentRequest
\x1a\x18
.memos.api.v1.Attachment
\"
T
\xda
A
\x16
attachment,update_mask
\x82\xd3\xe4\x93\x02
5:
\n
"
+
"attachment2'/api/v1/{attachment.name=attachments/*}
\x12
~
\n
"
+
"
\x10
DeleteAttachment
\x12
%.memos.api.v1.DeleteAttachmentRequest
\x1a\x16
.google.protobuf.Empty
\"
+
\xda
A
\x04
name
\x82\xd3\xe4\x93\x02\x1e
*
\x1c
/api/v1/{name=attachments/*}B
\xae\x01\n
"
+
...
...
web/src/components/AttachmentIcon.tsx
View file @
240d89fb
...
...
@@ -9,11 +9,11 @@ import {
FileVideo2Icon
,
SheetIcon
,
}
from
"lucide-react"
;
import
React
from
"react"
;
import
React
,
{
useState
}
from
"react"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
Attachment
}
from
"@/types/proto/api/v1/attachment_service"
;
import
{
getAttachmentType
,
getAttachmentUrl
}
from
"@/utils/attachment"
;
import
showPreviewImageDialog
from
"./PreviewImageDialog"
;
import
{
PreviewImageDialog
}
from
"./PreviewImageDialog"
;
import
SquareDiv
from
"./kit/SquareDiv"
;
interface
Props
{
...
...
@@ -24,26 +24,52 @@ interface Props {
const
AttachmentIcon
=
(
props
:
Props
)
=>
{
const
{
attachment
}
=
props
;
const
[
previewImage
,
setPreviewImage
]
=
useState
<
{
open
:
boolean
;
urls
:
string
[];
index
:
number
}
>
({
open
:
false
,
urls
:
[],
index
:
0
,
});
const
resourceType
=
getAttachmentType
(
attachment
);
const
resource
Url
=
getAttachmentUrl
(
attachment
);
const
attachment
Url
=
getAttachmentUrl
(
attachment
);
const
className
=
cn
(
"w-full h-auto"
,
props
.
className
);
const
strokeWidth
=
props
.
strokeWidth
;
const
previewResource
=
()
=>
{
window
.
open
(
resourceUrl
);
window
.
open
(
attachmentUrl
);
};
const
handleImageClick
=
()
=>
{
setPreviewImage
({
open
:
true
,
urls
:
[
attachmentUrl
],
index
:
0
});
};
if
(
resourceType
===
"image/*"
)
{
return
(
<
SquareDiv
className=
{
cn
(
className
,
"flex items-center justify-center overflow-clip"
)
}
>
<
img
className=
"min-w-full min-h-full object-cover"
src=
{
attachment
.
externalLink
?
resourceUrl
:
resourceUrl
+
"?thumbnail=true"
}
onClick=
{
()
=>
showPreviewImageDialog
(
resourceUrl
)
}
decoding=
"async"
loading=
"lazy"
<>
<
SquareDiv
className=
{
cn
(
className
,
"flex items-center justify-center overflow-clip"
)
}
>
<
img
className=
"min-w-full min-h-full object-cover"
src=
{
attachment
.
externalLink
?
attachmentUrl
:
attachmentUrl
+
"?thumbnail=true"
}
onClick=
{
handleImageClick
}
onError=
{
(
e
)
=>
{
// Fallback to original image if thumbnail fails
const
target
=
e
.
target
as
HTMLImageElement
;
if
(
target
.
src
.
includes
(
"?thumbnail=true"
))
{
console
.
warn
(
"Thumbnail failed, falling back to original image:"
,
attachmentUrl
);
target
.
src
=
attachmentUrl
;
}
}
}
decoding=
"async"
loading=
"lazy"
/>
</
SquareDiv
>
<
PreviewImageDialog
open=
{
previewImage
.
open
}
onOpenChange=
{
(
open
)
=>
setPreviewImage
((
prev
)
=>
({
...
prev
,
open
}))
}
imgUrls=
{
previewImage
.
urls
}
initialIndex=
{
previewImage
.
index
}
/>
</
SquareDiv
>
</>
);
}
...
...
web/src/components/ChangeMemberPasswordDialog.tsx
View file @
240d89fb
import
{
XIcon
}
from
"lucide-react"
;
import
{
useEffect
,
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Dialog
,
DialogContent
,
DialogFooter
,
DialogHeader
,
DialogTitle
}
from
"@/components/ui/dialog"
;
import
{
Input
}
from
"@/components/ui/input"
;
import
{
Label
}
from
"@/components/ui/label"
;
import
{
userStore
}
from
"@/store/v2"
;
import
{
User
}
from
"@/types/proto/api/v1/user_service"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
generateDialog
}
from
"./Dialog"
;
interface
Props
extends
DialogProps
{
user
:
User
;
interface
ChangeMemberPasswordDialogProps
{
open
:
boolean
;
onOpenChange
:
(
open
:
boolean
)
=>
void
;
user
?:
User
;
onSuccess
?:
()
=>
void
;
}
const
ChangeMemberPasswordDialog
:
React
.
FC
<
Props
>
=
(
props
:
Props
)
=>
{
const
{
user
,
destroy
}
=
props
;
export
function
ChangeMemberPasswordDialog
({
open
,
onOpenChange
,
user
,
onSuccess
}:
ChangeMemberPasswordDialogProps
)
{
const
t
=
useTranslate
();
const
[
newPassword
,
setNewPassword
]
=
useState
(
""
);
const
[
newPasswordAgain
,
setNewPasswordAgain
]
=
useState
(
""
);
...
...
@@ -23,7 +25,7 @@ const ChangeMemberPasswordDialog: React.FC<Props> = (props: Props) => {
},
[]);
const
handleCloseBtnClick
=
()
=>
{
destroy
(
);
onOpenChange
(
false
);
};
const
handleNewPasswordChanged
=
(
e
:
React
.
ChangeEvent
<
HTMLInputElement
>
)
=>
{
...
...
@@ -37,6 +39,8 @@ const ChangeMemberPasswordDialog: React.FC<Props> = (props: Props) => {
};
const
handleSaveBtnClick
=
async
()
=>
{
if
(
!
user
)
return
;
if
(
newPassword
===
""
||
newPasswordAgain
===
""
)
{
toast
.
error
(
t
(
"message.fill-all"
));
return
;
...
...
@@ -57,62 +61,55 @@ const ChangeMemberPasswordDialog: React.FC<Props> = (props: Props) => {
[
"password"
],
);
toast
(
t
(
"message.password-changed"
));
handleCloseBtnClick
();
onSuccess
?.();
onOpenChange
(
false
);
}
catch
(
error
:
any
)
{
console
.
error
(
error
);
toast
.
error
(
error
.
details
);
}
};
if
(
!
user
)
return
null
;
return
(
<
div
className=
"max-w-full shadow flex flex-col justify-start items-start bg-card text-card-foreground p-4 rounded-lg"
>
<
div
className=
"flex flex-row justify-between items-center mb-4 gap-2 w-full"
>
<
p
>
{
t
(
"setting.account-section.change-password"
)
}
(
{
user
.
displayName
}
)
</
p
>
<
Button
variant=
"ghost"
onClick=
{
handleCloseBtnClick
}
>
<
XIcon
className=
"w-5 h-auto"
/>
</
Button
>
</
div
>
<
div
className=
"flex flex-col justify-start items-start w-80!"
>
<
p
className=
"text-sm mb-1"
>
{
t
(
"auth.new-password"
)
}
</
p
>
<
Input
className=
"w-full"
type=
"password"
placeholder=
{
t
(
"auth.new-password"
)
}
value=
{
newPassword
}
onChange=
{
handleNewPasswordChanged
}
/>
<
p
className=
"text-sm mb-1 mt-2"
>
{
t
(
"auth.repeat-new-password"
)
}
</
p
>
<
Input
className=
"w-full"
type=
"password"
placeholder=
{
t
(
"auth.repeat-new-password"
)
}
value=
{
newPasswordAgain
}
onChange=
{
handleNewPasswordAgainChanged
}
/>
<
div
className=
"flex flex-row justify-end items-center mt-4 w-full gap-x-2"
>
<
Dialog
open=
{
open
}
onOpenChange=
{
onOpenChange
}
>
<
DialogContent
className=
"max-w-md"
>
<
DialogHeader
>
<
DialogTitle
>
{
t
(
"setting.account-section.change-password"
)
}
(
{
user
.
displayName
}
)
</
DialogTitle
>
</
DialogHeader
>
<
div
className=
"flex flex-col gap-4"
>
<
div
className=
"grid gap-2"
>
<
Label
htmlFor=
"newPassword"
>
{
t
(
"auth.new-password"
)
}
</
Label
>
<
Input
id=
"newPassword"
type=
"password"
placeholder=
{
t
(
"auth.new-password"
)
}
value=
{
newPassword
}
onChange=
{
handleNewPasswordChanged
}
/>
</
div
>
<
div
className=
"grid gap-2"
>
<
Label
htmlFor=
"newPasswordAgain"
>
{
t
(
"auth.repeat-new-password"
)
}
</
Label
>
<
Input
id=
"newPasswordAgain"
type=
"password"
placeholder=
{
t
(
"auth.repeat-new-password"
)
}
value=
{
newPasswordAgain
}
onChange=
{
handleNewPasswordAgainChanged
}
/>
</
div
>
</
div
>
<
DialogFooter
>
<
Button
variant=
"ghost"
onClick=
{
handleCloseBtnClick
}
>
{
t
(
"common.cancel"
)
}
</
Button
>
<
Button
color=
"primary"
onClick=
{
handleSaveBtnClick
}
>
{
t
(
"common.save"
)
}
</
Button
>
</
div
>
</
div
>
</
div
>
);
};
function
showChangeMemberPasswordDialog
(
user
:
User
)
{
generateDialog
(
{
className
:
"change-member-password-dialog"
,
dialogName
:
"change-member-password-dialog"
,
},
ChangeMemberPasswordDialog
,
{
user
},
<
Button
onClick=
{
handleSaveBtnClick
}
>
{
t
(
"common.save"
)
}
</
Button
>
</
DialogFooter
>
</
DialogContent
>
</
Dialog
>
);
}
export
default
show
ChangeMemberPasswordDialog
;
export
default
ChangeMemberPasswordDialog
;
web/src/components/CreateAccessTokenDialog.tsx
View file @
240d89fb
import
{
XIcon
}
from
"lucide-react"
;
import
React
,
{
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Dialog
,
DialogContent
,
DialogFooter
,
DialogHeader
,
DialogTitle
}
from
"@/components/ui/dialog"
;
import
{
Input
}
from
"@/components/ui/input"
;
import
{
Label
}
from
"@/components/ui/label"
;
import
{
RadioGroup
,
RadioGroupItem
}
from
"@/components/ui/radio-group"
;
...
...
@@ -9,10 +9,11 @@ import { userServiceClient } from "@/grpcweb";
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
useLoading
from
"@/hooks/useLoading"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
generateDialog
}
from
"./Dialog"
;
interface
Props
extends
DialogProps
{
onConfirm
:
()
=>
void
;
interface
CreateAccessTokenDialogProps
{
open
:
boolean
;
onOpenChange
:
(
open
:
boolean
)
=>
void
;
onSuccess
:
()
=>
void
;
}
interface
State
{
...
...
@@ -20,8 +21,7 @@ interface State {
expiration
:
number
;
}
const
CreateAccessTokenDialog
:
React
.
FC
<
Props
>
=
(
props
:
Props
)
=>
{
const
{
destroy
,
onConfirm
}
=
props
;
export
function
CreateAccessTokenDialog
({
open
,
onOpenChange
,
onSuccess
}:
CreateAccessTokenDialogProps
)
{
const
t
=
useTranslate
();
const
currentUser
=
useCurrentUser
();
const
[
state
,
setState
]
=
useState
({
...
...
@@ -71,6 +71,7 @@ const CreateAccessTokenDialog: React.FC<Props> = (props: Props) => {
}
try
{
requestState
.
setLoading
();
await
userServiceClient
.
createUserAccessToken
({
parent
:
currentUser
.
name
,
accessToken
:
{
...
...
@@ -79,42 +80,39 @@ const CreateAccessTokenDialog: React.FC<Props> = (props: Props) => {
},
});
onConfirm
();
destroy
();
requestState
.
setFinish
();
onSuccess
();
onOpenChange
(
false
);
}
catch
(
error
:
any
)
{
toast
.
error
(
error
.
details
);
console
.
error
(
error
);
requestState
.
setError
();
}
};
return
(
<
div
className=
"max-w-full shadow flex flex-col justify-start items-start bg-card text-card-foreground p-4 rounded-lg"
>
<
div
className=
"flex flex-row justify-between items-center w-full mb-4 gap-2"
>
<
p
>
{
t
(
"setting.access-token-section.create-dialog.create-access-token"
)
}
</
p
>
<
Button
variant=
"ghost"
onClick=
{
()
=>
destroy
()
}
>
<
XIcon
className=
"w-5 h-auto"
/>
</
Button
>
</
div
>
<
div
className=
"flex flex-col justify-start items-start w-80!"
>
<
div
className=
"w-full flex flex-col justify-start items-start mb-3"
>
<
span
className=
"mb-2"
>
{
t
(
"setting.access-token-section.create-dialog.description"
)
}
<
span
className=
"text-destructive"
>
*
</
span
>
</
span
>
<
div
className=
"relative w-full"
>
<
Dialog
open=
{
open
}
onOpenChange=
{
onOpenChange
}
>
<
DialogContent
className=
"max-w-md"
>
<
DialogHeader
>
<
DialogTitle
>
{
t
(
"setting.access-token-section.create-dialog.create-access-token"
)
}
</
DialogTitle
>
</
DialogHeader
>
<
div
className=
"flex flex-col gap-4"
>
<
div
className=
"grid gap-2"
>
<
Label
htmlFor=
"description"
>
{
t
(
"setting.access-token-section.create-dialog.description"
)
}
<
span
className=
"text-destructive"
>
*
</
span
>
</
Label
>
<
Input
className=
"w-full
"
id=
"description
"
type=
"text"
placeholder=
{
t
(
"setting.access-token-section.create-dialog.some-description"
)
}
value=
{
state
.
description
}
onChange=
{
handleDescriptionInputChange
}
/>
</
div
>
</
div
>
<
div
className=
"w-full flex flex-col justify-start items-start mb-3"
>
<
span
className=
"mb-2"
>
{
t
(
"setting.access-token-section.create-dialog.expiration"
)
}
<
span
className=
"text-destructive"
>
*
</
span
>
</
span
>
<
div
className=
"w-full flex flex-row justify-start items-center text-base"
>
<
div
className=
"grid gap-2"
>
<
Label
>
{
t
(
"setting.access-token-section.create-dialog.expiration"
)
}
<
span
className=
"text-destructive"
>
*
</
span
>
</
Label
>
<
RadioGroup
value=
{
state
.
expiration
.
toString
()
}
onValueChange=
{
handleRoleInputChange
}
className=
"flex flex-row gap-4"
>
{
expirationOptions
.
map
((
option
)
=>
(
<
div
key=
{
option
.
value
}
className=
"flex items-center space-x-2"
>
...
...
@@ -125,30 +123,17 @@ const CreateAccessTokenDialog: React.FC<Props> = (props: Props) => {
</
RadioGroup
>
</
div
>
</
div
>
<
div
className=
"w-full flex flex-row justify-end items-center mt-4 space-x-2"
>
<
Button
variant=
"ghost"
disabled=
{
requestState
.
isLoading
}
onClick=
{
destroy
}
>
<
DialogFooter
>
<
Button
variant=
"ghost"
disabled=
{
requestState
.
isLoading
}
onClick=
{
()
=>
onOpenChange
(
false
)
}
>
{
t
(
"common.cancel"
)
}
</
Button
>
<
Button
disabled=
{
requestState
.
isLoading
}
onClick=
{
handleSaveBtnClick
}
>
{
t
(
"common.create"
)
}
</
Button
>
</
div
>
</
div
>
</
div
>
);
};
function
showCreateAccessTokenDialog
(
onConfirm
:
()
=>
void
)
{
generateDialog
(
{
className
:
"create-access-token-dialog"
,
dialogName
:
"create-access-token-dialog"
,
},
CreateAccessTokenDialog
,
{
onConfirm
,
},
</
DialogFooter
>
</
DialogContent
>
</
Dialog
>
);
}
export
default
show
CreateAccessTokenDialog
;
export
default
CreateAccessTokenDialog
;
web/src/components/CreateIdentityProviderDialog.tsx
View file @
240d89fb
import
{
XIcon
}
from
"lucide-react"
;
import
{
useEffect
,
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Dialog
,
DialogContent
,
DialogFooter
,
DialogHeader
,
DialogTitle
}
from
"@/components/ui/dialog"
;
import
{
Input
}
from
"@/components/ui/input"
;
import
{
Select
,
SelectContent
,
SelectItem
,
SelectTrigger
,
SelectValue
}
from
"@/components/ui/select"
;
import
{
Separator
}
from
"@/components/ui/separator"
;
...
...
@@ -9,7 +9,6 @@ import { identityProviderServiceClient } from "@/grpcweb";
import
{
absolutifyLink
}
from
"@/helpers/utils"
;
import
{
FieldMapping
,
IdentityProvider
,
IdentityProvider_Type
,
OAuth2Config
}
from
"@/types/proto/api/v1/idp_service"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
generateDialog
}
from
"./Dialog"
;
const
templateList
:
IdentityProvider
[]
=
[
{
...
...
@@ -98,15 +97,16 @@ const templateList: IdentityProvider[] = [
},
];
interface
Props
extends
DialogProps
{
interface
CreateIdentityProviderDialogProps
{
open
:
boolean
;
onOpenChange
:
(
open
:
boolean
)
=>
void
;
identityProvider
?:
IdentityProvider
;
confirmCallback
?:
()
=>
void
;
onSuccess
?:
()
=>
void
;
}
const
CreateIdentityProviderDialog
:
React
.
FC
<
Props
>
=
(
props
:
Props
)
=>
{
export
function
CreateIdentityProviderDialog
({
open
,
onOpenChange
,
identityProvider
,
onSuccess
}:
CreateIdentityProviderDialogProps
)
{
const
t
=
useTranslate
();
const
identityProviderTypes
=
[...
new
Set
(
templateList
.
map
((
t
)
=>
t
.
type
))];
const
{
confirmCallback
,
destroy
,
identityProvider
}
=
props
;
const
[
basicInfo
,
setBasicInfo
]
=
useState
({
title
:
""
,
identifierFilter
:
""
,
...
...
@@ -165,7 +165,7 @@ const CreateIdentityProviderDialog: React.FC<Props> = (props: Props) => {
},
[
selectedTemplate
]);
const
handleCloseBtnClick
=
()
=>
{
destroy
(
);
onOpenChange
(
false
);
};
const
allowConfirmAction
=
()
=>
{
...
...
@@ -230,10 +230,8 @@ const CreateIdentityProviderDialog: React.FC<Props> = (props: Props) => {
toast
.
error
(
error
.
details
);
console
.
error
(
error
);
}
if
(
confirmCallback
)
{
confirmCallback
();
}
destroy
();
onSuccess
?.();
onOpenChange
(
false
);
};
const
setPartialOAuth2Config
=
(
state
:
Partial
<
OAuth2Config
>
)
=>
{
...
...
@@ -244,204 +242,192 @@ const CreateIdentityProviderDialog: React.FC<Props> = (props: Props) => {
};
return
(
<
div
className=
"max-w-full shadow flex flex-col justify-start items-start bg-card text-card-foreground p-4 rounded-lg"
>
<
div
className=
"flex flex-row justify-between items-center mb-4 gap-2 w-full"
>
<
p
>
{
t
(
isCreating
?
"setting.sso-section.create-sso"
:
"setting.sso-section.update-sso"
)
}
</
p
>
<
Button
variant=
"ghost"
onClick=
{
handleCloseBtnClick
}
>
<
XIcon
className=
"w-5 h-auto"
/>
</
Button
>
</
div
>
<
div
className=
"flex flex-col justify-start items-start w-80"
>
{
isCreating
&&
(
<>
<
p
className=
"mb-1!"
>
{
t
(
"common.type"
)
}
</
p
>
<
Select
value=
{
String
(
type
)
}
onValueChange=
{
(
value
)
=>
setType
(
parseInt
(
value
)
as
unknown
as
IdentityProvider_Type
)
}
>
<
SelectTrigger
className=
"w-full mb-4"
>
<
SelectValue
/>
</
SelectTrigger
>
<
SelectContent
>
{
identityProviderTypes
.
map
((
kind
)
=>
(
<
SelectItem
key=
{
kind
}
value=
{
String
(
kind
)
}
>
{
IdentityProvider_Type
[
kind
]
||
kind
}
</
SelectItem
>
))
}
</
SelectContent
>
</
Select
>
<
p
className=
"mb-2 text-sm font-medium"
>
{
t
(
"setting.sso-section.template"
)
}
</
p
>
<
Select
value=
{
selectedTemplate
}
onValueChange=
{
(
value
)
=>
setSelectedTemplate
(
value
)
}
>
<
SelectTrigger
className=
"mb-1 h-auto w-full"
>
<
SelectValue
/>
</
SelectTrigger
>
<
SelectContent
>
{
templateList
.
map
((
template
)
=>
(
<
SelectItem
key=
{
template
.
title
}
value=
{
template
.
title
}
>
{
template
.
title
}
</
SelectItem
>
))
}
</
SelectContent
>
</
Select
>
<
Separator
className=
"my-2"
/>
</>
)
}
<
p
className=
"mb-1 text-sm font-medium"
>
{
t
(
"common.name"
)
}
<
span
className=
"text-destructive"
>
*
</
span
>
</
p
>
<
Input
className=
"mb-2 w-full"
placeholder=
{
t
(
"common.name"
)
}
value=
{
basicInfo
.
title
}
onChange=
{
(
e
)
=>
setBasicInfo
({
...
basicInfo
,
title
:
e
.
target
.
value
,
})
}
/>
<
p
className=
"mb-1 text-sm font-medium"
>
{
t
(
"setting.sso-section.identifier-filter"
)
}
</
p
>
<
Input
className=
"mb-2 w-full"
placeholder=
{
t
(
"setting.sso-section.identifier-filter"
)
}
value=
{
basicInfo
.
identifierFilter
}
onChange=
{
(
e
)
=>
setBasicInfo
({
...
basicInfo
,
identifierFilter
:
e
.
target
.
value
,
})
}
/>
<
Separator
className=
"my-2"
/>
{
type
===
"OAUTH2"
&&
(
<>
{
isCreating
&&
(
<
p
className=
"border border-border rounded-md p-2 text-sm w-full mb-2 break-all"
>
{
t
(
"setting.sso-section.redirect-url"
)
}
:
{
absolutifyLink
(
"/auth/callback"
)
}
<
Dialog
open=
{
open
}
onOpenChange=
{
onOpenChange
}
>
<
DialogContent
className=
"max-w-2xl max-h-[80vh] overflow-y-auto"
>
<
DialogHeader
>
<
DialogTitle
>
{
t
(
isCreating
?
"setting.sso-section.create-sso"
:
"setting.sso-section.update-sso"
)
}
</
DialogTitle
>
</
DialogHeader
>
<
div
className=
"flex flex-col justify-start items-start w-full space-y-4"
>
{
isCreating
&&
(
<>
<
p
className=
"mb-1!"
>
{
t
(
"common.type"
)
}
</
p
>
<
Select
value=
{
String
(
type
)
}
onValueChange=
{
(
value
)
=>
setType
(
parseInt
(
value
)
as
unknown
as
IdentityProvider_Type
)
}
>
<
SelectTrigger
className=
"w-full mb-4"
>
<
SelectValue
/>
</
SelectTrigger
>
<
SelectContent
>
{
identityProviderTypes
.
map
((
kind
)
=>
(
<
SelectItem
key=
{
kind
}
value=
{
String
(
kind
)
}
>
{
IdentityProvider_Type
[
kind
]
||
kind
}
</
SelectItem
>
))
}
</
SelectContent
>
</
Select
>
<
p
className=
"mb-2 text-sm font-medium"
>
{
t
(
"setting.sso-section.template"
)
}
</
p
>
<
Select
value=
{
selectedTemplate
}
onValueChange=
{
(
value
)
=>
setSelectedTemplate
(
value
)
}
>
<
SelectTrigger
className=
"mb-1 h-auto w-full"
>
<
SelectValue
/>
</
SelectTrigger
>
<
SelectContent
>
{
templateList
.
map
((
template
)
=>
(
<
SelectItem
key=
{
template
.
title
}
value=
{
template
.
title
}
>
{
template
.
title
}
</
SelectItem
>
))
}
</
SelectContent
>
</
Select
>
<
Separator
className=
"my-2"
/>
</>
)
}
<
p
className=
"mb-1 text-sm font-medium"
>
{
t
(
"common.name"
)
}
<
span
className=
"text-destructive"
>
*
</
span
>
</
p
>
<
Input
className=
"mb-2 w-full"
placeholder=
{
t
(
"common.name"
)
}
value=
{
basicInfo
.
title
}
onChange=
{
(
e
)
=>
setBasicInfo
({
...
basicInfo
,
title
:
e
.
target
.
value
,
})
}
/>
<
p
className=
"mb-1 text-sm font-medium"
>
{
t
(
"setting.sso-section.identifier-filter"
)
}
</
p
>
<
Input
className=
"mb-2 w-full"
placeholder=
{
t
(
"setting.sso-section.identifier-filter"
)
}
value=
{
basicInfo
.
identifierFilter
}
onChange=
{
(
e
)
=>
setBasicInfo
({
...
basicInfo
,
identifierFilter
:
e
.
target
.
value
,
})
}
/>
<
Separator
className=
"my-2"
/>
{
type
===
"OAUTH2"
&&
(
<>
{
isCreating
&&
(
<
p
className=
"border border-border rounded-md p-2 text-sm w-full mb-2 break-all"
>
{
t
(
"setting.sso-section.redirect-url"
)
}
:
{
absolutifyLink
(
"/auth/callback"
)
}
</
p
>
)
}
<
p
className=
"mb-1 text-sm font-medium"
>
{
t
(
"setting.sso-section.client-id"
)
}
<
span
className=
"text-destructive"
>
*
</
span
>
</
p
>
<
Input
className=
"mb-2 w-full"
placeholder=
{
t
(
"setting.sso-section.client-id"
)
}
value=
{
oauth2Config
.
clientId
}
onChange=
{
(
e
)
=>
setPartialOAuth2Config
({
clientId
:
e
.
target
.
value
})
}
/>
<
p
className=
"mb-1 text-sm font-medium"
>
{
t
(
"setting.sso-section.client-secret"
)
}
<
span
className=
"text-destructive"
>
*
</
span
>
</
p
>
<
Input
className=
"mb-2 w-full"
placeholder=
{
t
(
"setting.sso-section.client-secret"
)
}
value=
{
oauth2Config
.
clientSecret
}
onChange=
{
(
e
)
=>
setPartialOAuth2Config
({
clientSecret
:
e
.
target
.
value
})
}
/>
<
p
className=
"mb-1 text-sm font-medium"
>
{
t
(
"setting.sso-section.authorization-endpoint"
)
}
<
span
className=
"text-destructive"
>
*
</
span
>
</
p
>
<
Input
className=
"mb-2 w-full"
placeholder=
{
t
(
"setting.sso-section.authorization-endpoint"
)
}
value=
{
oauth2Config
.
authUrl
}
onChange=
{
(
e
)
=>
setPartialOAuth2Config
({
authUrl
:
e
.
target
.
value
})
}
/>
<
p
className=
"mb-1 text-sm font-medium"
>
{
t
(
"setting.sso-section.token-endpoint"
)
}
<
span
className=
"text-destructive"
>
*
</
span
>
</
p
>
<
Input
className=
"mb-2 w-full"
placeholder=
{
t
(
"setting.sso-section.token-endpoint"
)
}
value=
{
oauth2Config
.
tokenUrl
}
onChange=
{
(
e
)
=>
setPartialOAuth2Config
({
tokenUrl
:
e
.
target
.
value
})
}
/>
<
p
className=
"mb-1 text-sm font-medium"
>
{
t
(
"setting.sso-section.user-endpoint"
)
}
<
span
className=
"text-destructive"
>
*
</
span
>
</
p
>
<
Input
className=
"mb-2 w-full"
placeholder=
{
t
(
"setting.sso-section.user-endpoint"
)
}
value=
{
oauth2Config
.
userInfoUrl
}
onChange=
{
(
e
)
=>
setPartialOAuth2Config
({
userInfoUrl
:
e
.
target
.
value
})
}
/>
<
p
className=
"mb-1 text-sm font-medium"
>
{
t
(
"setting.sso-section.scopes"
)
}
<
span
className=
"text-destructive"
>
*
</
span
>
</
p
>
)
}
<
p
className=
"mb-1 text-sm font-medium"
>
{
t
(
"setting.sso-section.client-id"
)
}
<
span
className=
"text-destructive"
>
*
</
span
>
</
p
>
<
Input
className=
"mb-2 w-full"
placeholder=
{
t
(
"setting.sso-section.client-id"
)
}
value=
{
oauth2Config
.
clientId
}
onChange=
{
(
e
)
=>
setPartialOAuth2Config
({
clientId
:
e
.
target
.
value
})
}
/>
<
p
className=
"mb-1 text-sm font-medium"
>
{
t
(
"setting.sso-section.client-secret"
)
}
<
span
className=
"text-destructive"
>
*
</
span
>
</
p
>
<
Input
className=
"mb-2 w-full"
placeholder=
{
t
(
"setting.sso-section.client-secret"
)
}
value=
{
oauth2Config
.
clientSecret
}
onChange=
{
(
e
)
=>
setPartialOAuth2Config
({
clientSecret
:
e
.
target
.
value
})
}
/>
<
p
className=
"mb-1 text-sm font-medium"
>
{
t
(
"setting.sso-section.authorization-endpoint"
)
}
<
span
className=
"text-destructive"
>
*
</
span
>
</
p
>
<
Input
className=
"mb-2 w-full"
placeholder=
{
t
(
"setting.sso-section.authorization-endpoint"
)
}
value=
{
oauth2Config
.
authUrl
}
onChange=
{
(
e
)
=>
setPartialOAuth2Config
({
authUrl
:
e
.
target
.
value
})
}
/>
<
p
className=
"mb-1 text-sm font-medium"
>
{
t
(
"setting.sso-section.token-endpoint"
)
}
<
span
className=
"text-destructive"
>
*
</
span
>
</
p
>
<
Input
className=
"mb-2 w-full"
placeholder=
{
t
(
"setting.sso-section.token-endpoint"
)
}
value=
{
oauth2Config
.
tokenUrl
}
onChange=
{
(
e
)
=>
setPartialOAuth2Config
({
tokenUrl
:
e
.
target
.
value
})
}
/>
<
p
className=
"mb-1 text-sm font-medium"
>
{
t
(
"setting.sso-section.user-endpoint"
)
}
<
span
className=
"text-destructive"
>
*
</
span
>
</
p
>
<
Input
className=
"mb-2 w-full"
placeholder=
{
t
(
"setting.sso-section.user-endpoint"
)
}
value=
{
oauth2Config
.
userInfoUrl
}
onChange=
{
(
e
)
=>
setPartialOAuth2Config
({
userInfoUrl
:
e
.
target
.
value
})
}
/>
<
p
className=
"mb-1 text-sm font-medium"
>
{
t
(
"setting.sso-section.scopes"
)
}
<
span
className=
"text-destructive"
>
*
</
span
>
</
p
>
<
Input
className=
"mb-2 w-full"
placeholder=
{
t
(
"setting.sso-section.scopes"
)
}
value=
{
oauth2Scopes
}
onChange=
{
(
e
)
=>
setOAuth2Scopes
(
e
.
target
.
value
)
}
/>
<
Separator
className=
"my-2"
/>
<
p
className=
"mb-1 text-sm font-medium"
>
{
t
(
"setting.sso-section.identifier"
)
}
<
span
className=
"text-destructive"
>
*
</
span
>
</
p
>
<
Input
className=
"mb-2 w-full"
placeholder=
{
t
(
"setting.sso-section.identifier"
)
}
value=
{
oauth2Config
.
fieldMapping
!
.
identifier
}
onChange=
{
(
e
)
=>
setPartialOAuth2Config
({
fieldMapping
:
{
...
oauth2Config
.
fieldMapping
,
identifier
:
e
.
target
.
value
}
as
FieldMapping
})
}
/>
<
p
className=
"mb-1 text-sm font-medium"
>
{
t
(
"setting.sso-section.display-name"
)
}
</
p
>
<
Input
className=
"mb-2 w-full"
placeholder=
{
t
(
"setting.sso-section.display-name"
)
}
value=
{
oauth2Config
.
fieldMapping
!
.
displayName
}
onChange=
{
(
e
)
=>
setPartialOAuth2Config
({
fieldMapping
:
{
...
oauth2Config
.
fieldMapping
,
displayName
:
e
.
target
.
value
}
as
FieldMapping
})
}
/>
<
p
className=
"mb-1 text-sm font-medium"
>
{
t
(
"common.email"
)
}
</
p
>
<
Input
className=
"mb-2 w-full"
placeholder=
{
t
(
"common.email"
)
}
value=
{
oauth2Config
.
fieldMapping
!
.
email
}
onChange=
{
(
e
)
=>
setPartialOAuth2Config
({
fieldMapping
:
{
...
oauth2Config
.
fieldMapping
,
email
:
e
.
target
.
value
}
as
FieldMapping
})
}
/>
<
p
className=
"mb-1 text-sm font-medium"
>
Avatar URL
</
p
>
<
Input
className=
"mb-2 w-full"
placeholder=
{
"Avatar URL"
}
value=
{
oauth2Config
.
fieldMapping
!
.
avatarUrl
}
onChange=
{
(
e
)
=>
setPartialOAuth2Config
({
fieldMapping
:
{
...
oauth2Config
.
fieldMapping
,
avatarUrl
:
e
.
target
.
value
}
as
FieldMapping
})
}
/>
</>
)
}
<
div
className=
"mt-2 w-full flex flex-row justify-end items-center space-x-1"
>
<
Input
className=
"mb-2 w-full"
placeholder=
{
t
(
"setting.sso-section.scopes"
)
}
value=
{
oauth2Scopes
}
onChange=
{
(
e
)
=>
setOAuth2Scopes
(
e
.
target
.
value
)
}
/>
<
Separator
className=
"my-2"
/>
<
p
className=
"mb-1 text-sm font-medium"
>
{
t
(
"setting.sso-section.identifier"
)
}
<
span
className=
"text-destructive"
>
*
</
span
>
</
p
>
<
Input
className=
"mb-2 w-full"
placeholder=
{
t
(
"setting.sso-section.identifier"
)
}
value=
{
oauth2Config
.
fieldMapping
!
.
identifier
}
onChange=
{
(
e
)
=>
setPartialOAuth2Config
({
fieldMapping
:
{
...
oauth2Config
.
fieldMapping
,
identifier
:
e
.
target
.
value
}
as
FieldMapping
})
}
/>
<
p
className=
"mb-1 text-sm font-medium"
>
{
t
(
"setting.sso-section.display-name"
)
}
</
p
>
<
Input
className=
"mb-2 w-full"
placeholder=
{
t
(
"setting.sso-section.display-name"
)
}
value=
{
oauth2Config
.
fieldMapping
!
.
displayName
}
onChange=
{
(
e
)
=>
setPartialOAuth2Config
({
fieldMapping
:
{
...
oauth2Config
.
fieldMapping
,
displayName
:
e
.
target
.
value
}
as
FieldMapping
})
}
/>
<
p
className=
"mb-1 text-sm font-medium"
>
{
t
(
"common.email"
)
}
</
p
>
<
Input
className=
"mb-2 w-full"
placeholder=
{
t
(
"common.email"
)
}
value=
{
oauth2Config
.
fieldMapping
!
.
email
}
onChange=
{
(
e
)
=>
setPartialOAuth2Config
({
fieldMapping
:
{
...
oauth2Config
.
fieldMapping
,
email
:
e
.
target
.
value
}
as
FieldMapping
})
}
/>
<
p
className=
"mb-1 text-sm font-medium"
>
Avatar URL
</
p
>
<
Input
className=
"mb-2 w-full"
placeholder=
{
"Avatar URL"
}
value=
{
oauth2Config
.
fieldMapping
!
.
avatarUrl
}
onChange=
{
(
e
)
=>
setPartialOAuth2Config
({
fieldMapping
:
{
...
oauth2Config
.
fieldMapping
,
avatarUrl
:
e
.
target
.
value
}
as
FieldMapping
})
}
/>
</>
)
}
</
div
>
<
DialogFooter
>
<
Button
variant=
"ghost"
onClick=
{
handleCloseBtnClick
}
>
{
t
(
"common.cancel"
)
}
</
Button
>
<
Button
onClick=
{
handleConfirmBtnClick
}
disabled=
{
!
allowConfirmAction
()
}
>
{
t
(
isCreating
?
"common.create"
:
"common.update"
)
}
</
Button
>
</
div
>
</
div
>
</
div
>
);
};
function
showCreateIdentityProviderDialog
(
identityProvider
?:
IdentityProvider
,
confirmCallback
?:
()
=>
void
)
{
generateDialog
(
{
className
:
"create-identity-provider-dialog"
,
dialogName
:
"create-identity-provider-dialog"
,
},
CreateIdentityProviderDialog
,
{
identityProvider
,
confirmCallback
},
</
DialogFooter
>
</
DialogContent
>
</
Dialog
>
);
}
export
default
show
CreateIdentityProviderDialog
;
export
default
CreateIdentityProviderDialog
;
web/src/components/CreateShortcutDialog.tsx
View file @
240d89fb
import
{
XIcon
}
from
"lucide-react"
;
import
React
,
{
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Dialog
,
DialogContent
,
DialogFooter
,
DialogHeader
,
DialogTitle
}
from
"@/components/ui/dialog"
;
import
{
Input
}
from
"@/components/ui/input"
;
import
{
Label
}
from
"@/components/ui/label"
;
import
{
Textarea
}
from
"@/components/ui/textarea"
;
import
{
shortcutServiceClient
}
from
"@/grpcweb"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
...
...
@@ -10,23 +11,24 @@ import useLoading from "@/hooks/useLoading";
import
{
userStore
}
from
"@/store/v2"
;
import
{
Shortcut
}
from
"@/types/proto/api/v1/shortcut_service"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
generateDialog
}
from
"./Dialog"
;
interface
Props
extends
DialogProps
{
interface
CreateShortcutDialogProps
{
open
:
boolean
;
onOpenChange
:
(
open
:
boolean
)
=>
void
;
shortcut
?:
Shortcut
;
onSuccess
?:
()
=>
void
;
}
const
CreateShortcutDialog
:
React
.
FC
<
Props
>
=
(
props
:
Props
)
=>
{
const
{
destroy
}
=
props
;
export
function
CreateShortcutDialog
({
open
,
onOpenChange
,
shortcut
:
initialShortcut
,
onSuccess
}:
CreateShortcutDialogProps
)
{
const
t
=
useTranslate
();
const
user
=
useCurrentUser
();
const
[
shortcut
,
setShortcut
]
=
useState
<
Shortcut
>
({
name
:
props
.
s
hortcut
?.
name
||
""
,
title
:
props
.
s
hortcut
?.
title
||
""
,
filter
:
props
.
s
hortcut
?.
filter
||
""
,
name
:
initialS
hortcut
?.
name
||
""
,
title
:
initialS
hortcut
?.
title
||
""
,
filter
:
initialS
hortcut
?.
filter
||
""
,
});
const
requestState
=
useLoading
(
false
);
const
isCreating
=
!
props
.
s
hortcut
;
const
isCreating
=
!
initialS
hortcut
;
const
onShortcutTitleChange
=
(
e
:
React
.
ChangeEvent
<
HTMLInputElement
>
)
=>
{
setShortcut
({
...
shortcut
,
title
:
e
.
target
.
value
});
...
...
@@ -43,6 +45,7 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
}
try
{
requestState
.
setLoading
();
if
(
isCreating
)
{
await
shortcutServiceClient
.
createShortcut
({
parent
:
user
.
name
,
...
...
@@ -57,7 +60,7 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
await
shortcutServiceClient
.
updateShortcut
({
shortcut
:
{
...
shortcut
,
name
:
props
.
s
hortcut
!
.
name
,
// Keep the original resource name
name
:
initialS
hortcut
!
.
name
,
// Keep the original resource name
},
updateMask
:
[
"title"
,
"filter"
],
});
...
...
@@ -65,79 +68,74 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
}
// Refresh shortcuts.
await
userStore
.
fetchShortcuts
();
destroy
();
requestState
.
setFinish
();
onSuccess
?.();
onOpenChange
(
false
);
}
catch
(
error
:
any
)
{
console
.
error
(
error
);
toast
.
error
(
error
.
details
);
requestState
.
setError
();
}
};
return
(
<
div
className=
"max-w-full shadow flex flex-col justify-start items-start bg-card text-card-foreground p-4 rounded-lg"
>
<
div
className=
"flex flex-row justify-between items-center mb-4 gap-2 w-full"
>
<
p
className=
"title-text"
>
{
`${isCreating ? t("common.create") : t("common.edit")} ${t("common.shortcuts")}`
}
</
p
>
<
Button
variant=
"ghost"
onClick=
{
()
=>
destroy
()
}
>
<
XIcon
className=
"w-5 h-auto"
/>
</
Button
>
</
div
>
<
div
className=
"flex flex-col justify-start items-start 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"
>
{
t
(
"common.title"
)
}
</
span
>
<
Input
className=
"w-full"
type=
"text"
placeholder=
""
value=
{
shortcut
.
title
}
onChange=
{
onShortcutTitleChange
}
/>
<
span
className=
"text-sm whitespace-nowrap mt-3 mb-1"
>
{
t
(
"common.filter"
)
}
</
span
>
<
Textarea
className=
"w-full"
rows=
{
3
}
placeholder=
{
t
(
"common.shortcut-filter"
)
}
value=
{
shortcut
.
filter
}
onChange=
{
onShortcutFilterChange
}
/>
<
Dialog
open=
{
open
}
onOpenChange=
{
onOpenChange
}
>
<
DialogContent
className=
"max-w-md"
>
<
DialogHeader
>
<
DialogTitle
>
{
`${isCreating ? t("common.create") : t("common.edit")} ${t("common.shortcuts")}`
}
</
DialogTitle
>
</
DialogHeader
>
<
div
className=
"flex flex-col gap-4"
>
<
div
className=
"grid gap-2"
>
<
Label
htmlFor=
"title"
>
{
t
(
"common.title"
)
}
</
Label
>
<
Input
id=
"title"
type=
"text"
placeholder=
""
value=
{
shortcut
.
title
}
onChange=
{
onShortcutTitleChange
}
/>
</
div
>
<
div
className=
"grid gap-2"
>
<
Label
htmlFor=
"filter"
>
{
t
(
"common.filter"
)
}
</
Label
>
<
Textarea
id=
"filter"
rows=
{
3
}
placeholder=
{
t
(
"common.shortcut-filter"
)
}
value=
{
shortcut
.
filter
}
onChange=
{
onShortcutFilterChange
}
/>
</
div
>
<
div
className=
"text-sm text-muted-foreground"
>
<
p
className=
"mb-2"
>
{
t
(
"common.learn-more"
)
}
:
</
p
>
<
ul
className=
"list-disc list-inside space-y-1"
>
<
li
>
<
a
className=
"text-primary hover:underline"
href=
"https://www.usememos.com/docs/getting-started/shortcuts"
target=
"_blank"
rel=
"noopener noreferrer"
>
Docs - Shortcuts
</
a
>
</
li
>
<
li
>
<
a
className=
"text-primary hover:underline"
href=
"https://www.usememos.com/docs/getting-started/shortcuts#how-to-write-a-filter"
target=
"_blank"
rel=
"noopener noreferrer"
>
How to Write a Filter?
</
a
>
</
li
>
</
ul
>
</
div
>
</
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-primary hover:underline"
href=
"https://www.usememos.com/docs/getting-started/shortcuts"
target=
"_blank"
>
Docs - Shortcuts
</
a
>
</
li
>
<
li
>
<
a
className=
"text-sm text-primary hover:underline"
href=
"https://www.usememos.com/docs/getting-started/shortcuts#how-to-write-a-filter"
target=
"_blank"
>
How to Write a Filter?
</
a
>
</
li
>
</
ul
>
</
div
>
<
div
className=
"w-full flex flex-row justify-end items-center space-x-2 mt-2"
>
<
Button
variant=
"ghost"
disabled=
{
requestState
.
isLoading
}
onClick=
{
destroy
}
>
<
DialogFooter
>
<
Button
variant=
"ghost"
disabled=
{
requestState
.
isLoading
}
onClick=
{
()
=>
onOpenChange
(
false
)
}
>
{
t
(
"common.cancel"
)
}
</
Button
>
<
Button
color=
"primary"
disabled=
{
requestState
.
isLoading
}
onClick=
{
handleConfirm
}
>
<
Button
disabled=
{
requestState
.
isLoading
}
onClick=
{
handleConfirm
}
>
{
t
(
"common.confirm"
)
}
</
Button
>
</
div
>
</
div
>
</
div
>
);
};
function
showCreateShortcutDialog
(
props
:
Pick
<
Props
,
"shortcut"
>
)
{
generateDialog
(
{
className
:
"create-shortcut-dialog"
,
dialogName
:
"create-shortcut-dialog"
,
},
CreateShortcutDialog
,
props
,
</
DialogFooter
>
</
DialogContent
>
</
Dialog
>
);
}
export
default
show
CreateShortcutDialog
;
export
default
CreateShortcutDialog
;
web/src/components/CreateUserDialog.tsx
View file @
240d89fb
import
{
XIcon
}
from
"lucide-react"
;
import
{
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Dialog
,
DialogContent
,
DialogFooter
,
DialogHeader
,
DialogTitle
}
from
"@/components/ui/dialog"
;
import
{
Input
}
from
"@/components/ui/input"
;
import
{
Label
}
from
"@/components/ui/label"
;
import
{
RadioGroup
,
RadioGroupItem
}
from
"@/components/ui/radio-group"
;
...
...
@@ -9,19 +9,19 @@ import { userServiceClient } from "@/grpcweb";
import
useLoading
from
"@/hooks/useLoading"
;
import
{
User
,
User_Role
}
from
"@/types/proto/api/v1/user_service"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
generateDialog
}
from
"./Dialog"
;
interface
Props
extends
DialogProps
{
interface
CreateUserDialogProps
{
open
:
boolean
;
onOpenChange
:
(
open
:
boolean
)
=>
void
;
user
?:
User
;
confirmCallback
?:
()
=>
void
;
onSuccess
?:
()
=>
void
;
}
const
CreateUserDialog
:
React
.
FC
<
Props
>
=
(
props
:
Props
)
=>
{
const
{
confirmCallback
,
destroy
}
=
props
;
export
function
CreateUserDialog
({
open
,
onOpenChange
,
user
:
initialUser
,
onSuccess
}:
CreateUserDialogProps
)
{
const
t
=
useTranslate
();
const
[
user
,
setUser
]
=
useState
(
User
.
fromPartial
({
...
props
.
u
ser
}));
const
[
user
,
setUser
]
=
useState
(
User
.
fromPartial
({
...
initialU
ser
}));
const
requestState
=
useLoading
(
false
);
const
isCreating
=
!
props
.
u
ser
;
const
isCreating
=
!
initialU
ser
;
const
setPartialUser
=
(
state
:
Partial
<
User
>
)
=>
{
setUser
({
...
...
@@ -37,106 +37,99 @@ const CreateUserDialog: React.FC<Props> = (props: Props) => {
}
try
{
requestState
.
setLoading
();
if
(
isCreating
)
{
await
userServiceClient
.
createUser
({
user
});
toast
.
success
(
"Create user successfully"
);
}
else
{
const
updateMask
=
[];
if
(
user
.
username
!==
props
.
u
ser
?.
username
)
{
if
(
user
.
username
!==
initialU
ser
?.
username
)
{
updateMask
.
push
(
"username"
);
}
if
(
user
.
password
)
{
updateMask
.
push
(
"password"
);
}
if
(
user
.
role
!==
props
.
u
ser
?.
role
)
{
if
(
user
.
role
!==
initialU
ser
?.
role
)
{
updateMask
.
push
(
"role"
);
}
await
userServiceClient
.
updateUser
({
user
,
updateMask
});
toast
.
success
(
"Update user successfully"
);
}
requestState
.
setFinish
();
onSuccess
?.();
onOpenChange
(
false
);
}
catch
(
error
:
any
)
{
console
.
error
(
error
);
toast
.
error
(
error
.
details
);
requestState
.
setError
();
}
if
(
confirmCallback
)
{
confirmCallback
();
}
destroy
();
};
return
(
<
div
className=
"max-w-full shadow flex flex-col justify-start items-start bg-card text-card-foreground p-4 rounded-lg"
>
<
div
className=
"flex flex-row justify-between items-center mb-4 gap-2 w-full"
>
<
p
className=
"title-text"
>
{
`${isCreating ? t("common.create") : t("common.edit")} ${t("common.user")}`
}
</
p
>
<
Button
variant=
"ghost"
onClick=
{
()
=>
destroy
()
}
>
<
XIcon
className=
"w-5 h-auto"
/>
</
Button
>
</
div
>
<
div
className=
"flex flex-col justify-start items-start 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"
>
{
t
(
"common.username"
)
}
</
span
>
<
Input
className=
"w-full"
type=
"text"
placeholder=
{
t
(
"common.username"
)
}
value=
{
user
.
username
}
onChange=
{
(
e
)
=>
setPartialUser
({
username
:
e
.
target
.
value
,
})
}
/>
<
span
className=
"text-sm whitespace-nowrap mt-3 mb-1"
>
{
t
(
"common.password"
)
}
</
span
>
<
Input
className=
"w-full"
type=
"password"
placeholder=
{
t
(
"common.password"
)
}
autoComplete=
"off"
value=
{
user
.
password
}
onChange=
{
(
e
)
=>
setPartialUser
({
password
:
e
.
target
.
value
,
})
}
/>
<
span
className=
"text-sm whitespace-nowrap mt-3 mb-1"
>
{
t
(
"common.role"
)
}
</
span
>
<
RadioGroup
value=
{
user
.
role
}
onValueChange=
{
(
value
)
=>
setPartialUser
({
role
:
value
as
User_Role
})
}
className=
"flex flex-row gap-4"
>
<
div
className=
"flex items-center space-x-2"
>
<
RadioGroupItem
value=
{
User_Role
.
USER
}
id=
"user"
/>
<
Label
htmlFor=
"user"
>
{
t
(
"setting.member-section.user"
)
}
</
Label
>
</
div
>
<
div
className=
"flex items-center space-x-2"
>
<
RadioGroupItem
value=
{
User_Role
.
ADMIN
}
id=
"admin"
/>
<
Label
htmlFor=
"admin"
>
{
t
(
"setting.member-section.admin"
)
}
</
Label
>
</
div
>
</
RadioGroup
>
<
Dialog
open=
{
open
}
onOpenChange=
{
onOpenChange
}
>
<
DialogContent
className=
"max-w-md"
>
<
DialogHeader
>
<
DialogTitle
>
{
`${isCreating ? t("common.create") : t("common.edit")} ${t("common.user")}`
}
</
DialogTitle
>
</
DialogHeader
>
<
div
className=
"flex flex-col gap-4"
>
<
div
className=
"grid gap-2"
>
<
Label
htmlFor=
"username"
>
{
t
(
"common.username"
)
}
</
Label
>
<
Input
id=
"username"
type=
"text"
placeholder=
{
t
(
"common.username"
)
}
value=
{
user
.
username
}
onChange=
{
(
e
)
=>
setPartialUser
({
username
:
e
.
target
.
value
,
})
}
/>
</
div
>
<
div
className=
"grid gap-2"
>
<
Label
htmlFor=
"password"
>
{
t
(
"common.password"
)
}
</
Label
>
<
Input
id=
"password"
type=
"password"
placeholder=
{
t
(
"common.password"
)
}
autoComplete=
"off"
value=
{
user
.
password
}
onChange=
{
(
e
)
=>
setPartialUser
({
password
:
e
.
target
.
value
,
})
}
/>
</
div
>
<
div
className=
"grid gap-2"
>
<
Label
>
{
t
(
"common.role"
)
}
</
Label
>
<
RadioGroup
value=
{
user
.
role
}
onValueChange=
{
(
value
)
=>
setPartialUser
({
role
:
value
as
User_Role
})
}
className=
"flex flex-row gap-4"
>
<
div
className=
"flex items-center space-x-2"
>
<
RadioGroupItem
value=
{
User_Role
.
USER
}
id=
"user"
/>
<
Label
htmlFor=
"user"
>
{
t
(
"setting.member-section.user"
)
}
</
Label
>
</
div
>
<
div
className=
"flex items-center space-x-2"
>
<
RadioGroupItem
value=
{
User_Role
.
ADMIN
}
id=
"admin"
/>
<
Label
htmlFor=
"admin"
>
{
t
(
"setting.member-section.admin"
)
}
</
Label
>
</
div
>
</
RadioGroup
>
</
div
>
</
div
>
<
div
className=
"w-full flex flex-row justify-end items-center space-x-2 mt-2"
>
<
Button
variant=
"ghost"
disabled=
{
requestState
.
isLoading
}
onClick=
{
destroy
}
>
<
DialogFooter
>
<
Button
variant=
"ghost"
disabled=
{
requestState
.
isLoading
}
onClick=
{
()
=>
onOpenChange
(
false
)
}
>
{
t
(
"common.cancel"
)
}
</
Button
>
<
Button
color=
"primary"
disabled=
{
requestState
.
isLoading
}
onClick=
{
handleConfirm
}
>
<
Button
disabled=
{
requestState
.
isLoading
}
onClick=
{
handleConfirm
}
>
{
t
(
"common.confirm"
)
}
</
Button
>
</
div
>
</
div
>
</
div
>
);
};
function
showCreateUserDialog
(
user
?:
User
,
confirmCallback
?:
()
=>
void
)
{
generateDialog
(
{
className
:
"create-user-dialog"
,
dialogName
:
"create-user-dialog"
,
},
CreateUserDialog
,
{
user
,
confirmCallback
},
</
DialogFooter
>
</
DialogContent
>
</
Dialog
>
);
}
export
default
show
CreateUserDialog
;
export
default
CreateUserDialog
;
web/src/components/CreateWebhookDialog.tsx
View file @
240d89fb
import
{
XIcon
}
from
"lucide-react"
;
import
React
,
{
useEffect
,
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Dialog
,
DialogContent
,
DialogFooter
,
DialogHeader
,
DialogTitle
}
from
"@/components/ui/dialog"
;
import
{
Input
}
from
"@/components/ui/input"
;
import
{
Label
}
from
"@/components/ui/label"
;
import
{
webhookServiceClient
}
from
"@/grpcweb"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
useLoading
from
"@/hooks/useLoading"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
generateDialog
}
from
"./Dialog"
;
interface
Props
extends
DialogProps
{
interface
CreateWebhookDialogProps
{
open
:
boolean
;
onOpenChange
:
(
open
:
boolean
)
=>
void
;
webhookName
?:
string
;
on
Confirm
:
()
=>
void
;
on
Success
?
:
()
=>
void
;
}
interface
State
{
...
...
@@ -19,11 +21,10 @@ interface State {
url
:
string
;
}
const
CreateWebhookDialog
:
React
.
FC
<
Props
>
=
(
props
:
Props
)
=>
{
const
{
webhookName
,
destroy
,
onConfirm
}
=
props
;
export
function
CreateWebhookDialog
({
open
,
onOpenChange
,
webhookName
,
onSuccess
}:
CreateWebhookDialogProps
)
{
const
t
=
useTranslate
();
const
currentUser
=
useCurrentUser
();
const
[
state
,
setState
]
=
useState
({
const
[
state
,
setState
]
=
useState
<
State
>
({
displayName
:
""
,
url
:
""
,
});
...
...
@@ -43,7 +44,7 @@ const CreateWebhookDialog: React.FC<Props> = (props: Props) => {
});
});
}
},
[]);
},
[
webhookName
]);
const
setPartialState
=
(
partialState
:
Partial
<
State
>
)
=>
{
setState
({
...
...
@@ -76,6 +77,7 @@ const CreateWebhookDialog: React.FC<Props> = (props: Props) => {
}
try
{
requestState
.
setLoading
();
if
(
isCreating
)
{
await
webhookServiceClient
.
createWebhook
({
parent
:
currentUser
.
name
,
...
...
@@ -95,46 +97,45 @@ const CreateWebhookDialog: React.FC<Props> = (props: Props) => {
});
}
onConfirm
();
destroy
();
onSuccess
?.();
onOpenChange
(
false
);
requestState
.
setFinish
();
}
catch
(
error
:
any
)
{
console
.
error
(
error
);
toast
.
error
(
error
.
details
);
requestState
.
setError
();
}
};
return
(
<
div
className=
"max-w-full shadow flex flex-col justify-start items-start bg-card text-card-foreground p-4 rounded-lg"
>
<
div
className=
"flex flex-row justify-between items-center mb-4 gap-2 w-full"
>
<
p
className=
"title-text"
>
{
isCreating
?
t
(
"setting.webhook-section.create-dialog.create-webhook"
)
:
t
(
"setting.webhook-section.create-dialog.edit-webhook"
)
}
</
p
>
<
Button
variant=
"ghost"
onClick=
{
()
=>
destroy
()
}
>
<
XIcon
className=
"w-5 h-auto"
/>
</
Button
>
</
div
>
<
div
className=
"flex flex-col justify-start items-start w-80!"
>
<
div
className=
"w-full flex flex-col justify-start items-start mb-3"
>
<
span
className=
"mb-2"
>
{
t
(
"setting.webhook-section.create-dialog.title"
)
}
<
span
className=
"text-destructive"
>
*
</
span
>
</
span
>
<
div
className=
"relative w-full"
>
<
Dialog
open=
{
open
}
onOpenChange=
{
onOpenChange
}
>
<
DialogContent
className=
"max-w-md"
>
<
DialogHeader
>
<
DialogTitle
>
{
isCreating
?
t
(
"setting.webhook-section.create-dialog.create-webhook"
)
:
t
(
"setting.webhook-section.create-dialog.edit-webhook"
)
}
</
DialogTitle
>
</
DialogHeader
>
<
div
className=
"flex flex-col gap-4"
>
<
div
className=
"grid gap-2"
>
<
Label
htmlFor=
"displayName"
>
{
t
(
"setting.webhook-section.create-dialog.title"
)
}
<
span
className=
"text-destructive"
>
*
</
span
>
</
Label
>
<
Input
className=
"w-full
"
id=
"displayName
"
type=
"text"
placeholder=
{
t
(
"setting.webhook-section.create-dialog.an-easy-to-remember-name"
)
}
value=
{
state
.
displayName
}
onChange=
{
handleTitleInputChange
}
/>
</
div
>
</
div
>
<
div
className=
"w-full flex flex-col justify-start items-start mb-3"
>
<
span
className=
"mb-2"
>
{
t
(
"setting.webhook-section.create-dialog.payload-url"
)
}
<
span
className=
"text-destructive"
>
*
</
span
>
</
span
>
<
div
className=
"relative w-full"
>
<
div
className=
"grid gap-2"
>
<
Label
htmlFor=
"url"
>
{
t
(
"setting.webhook-section.create-dialog.payload-url"
)
}
<
span
className=
"text-destructive"
>
*
</
span
>
</
Label
>
<
Input
className=
"w-ful
l"
id=
"ur
l"
type=
"text"
placeholder=
{
t
(
"setting.webhook-section.create-dialog.url-example-post-receive"
)
}
value=
{
state
.
url
}
...
...
@@ -142,30 +143,17 @@ const CreateWebhookDialog: React.FC<Props> = (props: Props) => {
/>
</
div
>
</
div
>
<
div
className=
"w-full flex flex-row justify-end items-center mt-2 space-x-2"
>
<
Button
variant=
"ghost"
disabled=
{
requestState
.
isLoading
}
onClick=
{
destroy
}
>
<
DialogFooter
>
<
Button
variant=
"ghost"
disabled=
{
requestState
.
isLoading
}
onClick=
{
()
=>
onOpenChange
(
false
)
}
>
{
t
(
"common.cancel"
)
}
</
Button
>
<
Button
color=
"primary"
disabled=
{
requestState
.
isLoading
}
onClick=
{
handleSaveBtnClick
}
>
<
Button
disabled=
{
requestState
.
isLoading
}
onClick=
{
handleSaveBtnClick
}
>
{
t
(
"common.create"
)
}
</
Button
>
</
div
>
</
div
>
</
div
>
);
};
function
showCreateWebhookDialog
(
onConfirm
:
()
=>
void
)
{
generateDialog
(
{
className
:
"create-webhook-dialog"
,
dialogName
:
"create-webhook-dialog"
,
},
CreateWebhookDialog
,
{
onConfirm
,
},
</
DialogFooter
>
</
DialogContent
>
</
Dialog
>
);
}
export
default
show
CreateWebhookDialog
;
export
default
CreateWebhookDialog
;
web/src/components/Dialog/BaseDialog.tsx
deleted
100644 → 0
View file @
f7013853
import
{
observer
}
from
"mobx-react-lite"
;
import
{
useEffect
,
useRef
}
from
"react"
;
import
{
createRoot
}
from
"react-dom/client"
;
import
{
cn
}
from
"@/lib/utils"
;
import
dialogStore
from
"@/store/v2/dialog"
;
interface
DialogConfig
{
dialogName
:
string
;
className
?:
string
;
clickSpaceDestroy
?:
boolean
;
}
interface
Props
extends
DialogConfig
,
DialogProps
{
children
:
React
.
ReactNode
;
}
const
BaseDialog
=
observer
((
props
:
Props
)
=>
{
const
{
children
,
className
,
clickSpaceDestroy
,
dialogName
,
destroy
}
=
props
;
const
dialogContainerRef
=
useRef
<
HTMLDivElement
>
(
null
);
const
dialogIndex
=
dialogStore
.
state
.
stack
.
findIndex
((
item
)
=>
item
===
dialogName
);
useEffect
(()
=>
{
dialogStore
.
pushDialog
(
dialogName
);
const
handleKeyDown
=
(
event
:
KeyboardEvent
)
=>
{
if
(
event
.
code
===
"Escape"
)
{
if
(
dialogName
===
dialogStore
.
topDialog
)
{
destroy
();
}
}
};
document
.
body
.
addEventListener
(
"keydown"
,
handleKeyDown
);
return
()
=>
{
document
.
body
.
removeEventListener
(
"keydown"
,
handleKeyDown
);
dialogStore
.
removeDialog
(
dialogName
);
};
},
[]);
useEffect
(()
=>
{
if
(
dialogIndex
>
0
&&
dialogContainerRef
.
current
)
{
dialogContainerRef
.
current
.
style
.
marginTop
=
`
${
dialogIndex
*
16
}
px`
;
}
},
[
dialogIndex
]);
const
handleSpaceClicked
=
()
=>
{
if
(
clickSpaceDestroy
)
{
destroy
();
}
};
return
(
<
div
className=
{
cn
(
"fixed top-0 left-0 flex flex-col justify-start items-center w-full h-full pt-16 pb-8 px-4 z-50 overflow-x-hidden overflow-y-scroll transition-all hide-scrollbar bg-foreground/60"
,
className
,
)
}
onMouseDown=
{
handleSpaceClicked
}
>
<
div
ref=
{
dialogContainerRef
}
onMouseDown=
{
(
e
)
=>
e
.
stopPropagation
()
}
>
{
children
}
</
div
>
</
div
>
);
});
export
function
generateDialog
<
T
extends
DialogProps
>
(
config
:
DialogConfig
,
DialogComponent
:
React
.
FC
<
T
>
,
props
?:
Omit
<
T
,
"destroy"
>
,
):
DialogCallback
{
const
tempDiv
=
document
.
createElement
(
"div"
);
const
dialog
=
createRoot
(
tempDiv
);
document
.
body
.
append
(
tempDiv
);
document
.
body
.
style
.
overflow
=
"hidden"
;
const
cbs
:
DialogCallback
=
{
destroy
:
()
=>
{
document
.
body
.
style
.
removeProperty
(
"overflow"
);
dialog
.
unmount
();
tempDiv
.
remove
();
},
};
const
dialogProps
=
{
...
props
,
destroy
:
cbs
.
destroy
,
}
as
T
;
const
Fragment
=
observer
(()
=>
(
<
BaseDialog
destroy=
{
cbs
.
destroy
}
clickSpaceDestroy=
{
true
}
{
...
config
}
>
<
DialogComponent
{
...
dialogProps
}
/>
</
BaseDialog
>
));
dialog
.
render
(<
Fragment
/>);
return
cbs
;
}
web/src/components/Dialog/index.ts
deleted
100644 → 0
View file @
f7013853
export
{
generateDialog
}
from
"./BaseDialog"
;
web/src/components/HomeSidebar/ShortcutsSection.tsx
View file @
240d89fb
import
{
Edit3Icon
,
MoreVerticalIcon
,
TrashIcon
,
PlusIcon
}
from
"lucide-react"
;
import
{
observer
}
from
"mobx-react-lite"
;
import
{
useState
}
from
"react"
;
import
{
Tooltip
,
TooltipContent
,
TooltipProvider
,
TooltipTrigger
}
from
"@/components/ui/tooltip"
;
import
{
shortcutServiceClient
}
from
"@/grpcweb"
;
import
useAsyncEffect
from
"@/hooks/useAsyncEffect"
;
...
...
@@ -8,7 +9,7 @@ import { userStore } from "@/store/v2";
import
memoFilterStore
from
"@/store/v2/memoFilter"
;
import
{
Shortcut
}
from
"@/types/proto/api/v1/shortcut_service"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
show
CreateShortcutDialog
from
"../CreateShortcutDialog"
;
import
CreateShortcutDialog
from
"../CreateShortcutDialog"
;
import
{
DropdownMenu
,
DropdownMenuContent
,
DropdownMenuItem
,
DropdownMenuTrigger
}
from
"../ui/dropdown-menu"
;
const
emojiRegex
=
/^
(\p
{Emoji_Presentation}|
\p
{Emoji}
\u
FE0F
)
$/
u
;
...
...
@@ -23,6 +24,8 @@ const getShortcutId = (name: string): string => {
const
ShortcutsSection
=
observer
(()
=>
{
const
t
=
useTranslate
();
const
shortcuts
=
userStore
.
state
.
shortcuts
;
const
[
isCreateShortcutDialogOpen
,
setIsCreateShortcutDialogOpen
]
=
useState
(
false
);
const
[
editingShortcut
,
setEditingShortcut
]
=
useState
<
Shortcut
|
undefined
>
();
useAsyncEffect
(
async
()
=>
{
await
userStore
.
fetchShortcuts
();
...
...
@@ -36,6 +39,21 @@ const ShortcutsSection = observer(() => {
}
};
const
handleCreateShortcut
=
()
=>
{
setEditingShortcut
(
undefined
);
setIsCreateShortcutDialogOpen
(
true
);
};
const
handleEditShortcut
=
(
shortcut
:
Shortcut
)
=>
{
setEditingShortcut
(
shortcut
);
setIsCreateShortcutDialogOpen
(
true
);
};
const
handleShortcutDialogSuccess
=
()
=>
{
setIsCreateShortcutDialogOpen
(
false
);
setEditingShortcut
(
undefined
);
};
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-muted-foreground select-none"
>
...
...
@@ -43,7 +61,7 @@ const ShortcutsSection = observer(() => {
<
TooltipProvider
>
<
Tooltip
>
<
TooltipTrigger
asChild
>
<
PlusIcon
className=
"w-4 h-auto cursor-pointer"
onClick=
{
()
=>
showCreateShortcutDialog
({})
}
/>
<
PlusIcon
className=
"w-4 h-auto cursor-pointer"
onClick=
{
handleCreateShortcut
}
/>
</
TooltipTrigger
>
<
TooltipContent
>
<
p
>
{
t
(
"common.create"
)
}
</
p
>
...
...
@@ -75,7 +93,7 @@ const ShortcutsSection = observer(() => {
<
MoreVerticalIcon
className=
"w-4 h-auto shrink-0 text-muted-foreground cursor-pointer hover:text-foreground"
/>
</
DropdownMenuTrigger
>
<
DropdownMenuContent
align=
"end"
alignOffset=
{
-
12
}
>
<
DropdownMenuItem
onClick=
{
()
=>
showCreateShortcutDialog
({
shortcut
}
)
}
>
<
DropdownMenuItem
onClick=
{
()
=>
handleEditShortcut
(
shortcut
)
}
>
<
Edit3Icon
className=
"w-4 h-auto"
/>
{
t
(
"common.edit"
)
}
</
DropdownMenuItem
>
...
...
@@ -89,6 +107,12 @@ const ShortcutsSection = observer(() => {
);
})
}
</
div
>
<
CreateShortcutDialog
open=
{
isCreateShortcutDialogOpen
}
onOpenChange=
{
setIsCreateShortcutDialogOpen
}
shortcut=
{
editingShortcut
}
onSuccess=
{
handleShortcutDialogSuccess
}
/>
</
div
>
);
});
...
...
web/src/components/HomeSidebar/TagsSection.tsx
View file @
240d89fb
import
{
Edit3Icon
,
HashIcon
,
MoreVerticalIcon
,
TagsIcon
,
TrashIcon
}
from
"lucide-react"
;
import
{
observer
}
from
"mobx-react-lite"
;
import
{
useState
}
from
"react"
;
import
toast
from
"react-hot-toast"
;
import
useLocalStorage
from
"react-use/lib/useLocalStorage"
;
import
{
Switch
}
from
"@/components/ui/switch"
;
import
{
memoServiceClient
}
from
"@/grpcweb"
;
import
{
useDialog
}
from
"@/hooks/useDialog"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
userStore
}
from
"@/store/v2"
;
import
memoFilterStore
,
{
MemoFilter
}
from
"@/store/v2/memoFilter"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
show
RenameTagDialog
from
"../RenameTagDialog"
;
import
RenameTagDialog
from
"../RenameTagDialog"
;
import
TagTree
from
"../TagTree"
;
import
{
DropdownMenu
,
DropdownMenuContent
,
DropdownMenuItem
,
DropdownMenuTrigger
}
from
"../ui/dropdown-menu"
;
import
{
Popover
,
PopoverContent
,
PopoverTrigger
}
from
"../ui/popover"
;
...
...
@@ -20,6 +22,8 @@ interface Props {
const
TagsSection
=
observer
((
props
:
Props
)
=>
{
const
t
=
useTranslate
();
const
[
treeMode
,
setTreeMode
]
=
useLocalStorage
<
boolean
>
(
"tag-view-as-tree"
,
false
);
const
renameTagDialog
=
useDialog
();
const
[
selectedTag
,
setSelectedTag
]
=
useState
<
string
>
(
""
);
const
tags
=
Object
.
entries
(
userStore
.
state
.
tagCount
)
.
sort
((
a
,
b
)
=>
a
[
0
].
localeCompare
(
b
[
0
]))
.
sort
((
a
,
b
)
=>
b
[
1
]
-
a
[
1
]);
...
...
@@ -36,6 +40,16 @@ const TagsSection = observer((props: Props) => {
}
};
const
handleRenameTag
=
(
tag
:
string
)
=>
{
setSelectedTag
(
tag
);
renameTagDialog
.
open
();
};
const
handleRenameSuccess
=
()
=>
{
// Refresh tags after rename
userStore
.
fetchUsers
();
};
const
handleDeleteTag
=
async
(
tag
:
string
)
=>
{
const
confirmed
=
window
.
confirm
(
t
(
"tag.delete-confirm"
));
if
(
confirmed
)
{
...
...
@@ -83,7 +97,7 @@ const TagsSection = observer((props: Props) => {
</
div
>
</
DropdownMenuTrigger
>
<
DropdownMenuContent
align=
"start"
sideOffset=
{
2
}
>
<
DropdownMenuItem
onClick=
{
()
=>
showRenameTagDialog
({
tag
:
tag
}
)
}
>
<
DropdownMenuItem
onClick=
{
()
=>
handleRenameTag
(
tag
)
}
>
<
Edit3Icon
className=
"w-4 h-auto"
/>
{
t
(
"common.rename"
)
}
</
DropdownMenuItem
>
...
...
@@ -112,6 +126,14 @@ const TagsSection = observer((props: Props) => {
</
div
>
)
)
}
{
/* Rename Tag Dialog */
}
<
RenameTagDialog
open=
{
renameTagDialog
.
isOpen
}
onOpenChange=
{
renameTagDialog
.
setOpen
}
tag=
{
selectedTag
}
onSuccess=
{
handleRenameSuccess
}
/>
</
div
>
);
});
...
...
web/src/components/MemoAttachmentListView.tsx
View file @
240d89fb
import
{
memo
}
from
"react"
;
import
{
memo
,
useState
}
from
"react"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
Attachment
}
from
"@/types/proto/api/v1/attachment_service"
;
import
{
getAttachmentType
,
getAttachmentUrl
}
from
"@/utils/attachment"
;
import
MemoAttachment
from
"./MemoAttachment"
;
import
showPreviewImageDialog
from
"./PreviewImageDialog"
;
import
{
PreviewImageDialog
}
from
"./PreviewImageDialog"
;
const
MemoAttachmentListView
=
({
attachments
=
[]
}:
{
attachments
:
Attachment
[]
})
=>
{
const
[
previewImage
,
setPreviewImage
]
=
useState
<
{
open
:
boolean
;
urls
:
string
[];
index
:
number
}
>
({
open
:
false
,
urls
:
[],
index
:
0
,
});
const
mediaAttachments
:
Attachment
[]
=
[];
const
otherAttachments
:
Attachment
[]
=
[];
...
...
@@ -24,7 +29,7 @@ const MemoAttachmentListView = ({ attachments = [] }: { attachments: Attachment[
.
filter
((
attachment
)
=>
getAttachmentType
(
attachment
)
===
"image/*"
)
.
map
((
attachment
)
=>
getAttachmentUrl
(
attachment
));
const
index
=
imgUrls
.
findIndex
((
url
)
=>
url
===
imgUrl
);
s
howPreviewImageDialog
(
imgUrls
,
index
);
s
etPreviewImage
({
open
:
true
,
urls
:
imgUrls
,
index
}
);
};
const
MediaCard
=
({
attachment
,
className
}:
{
attachment
:
Attachment
;
className
?:
string
})
=>
{
...
...
@@ -39,6 +44,14 @@ const MemoAttachmentListView = ({ attachments = [] }: { attachments: Attachment[
className
,
)
}
src=
{
attachment
.
externalLink
?
attachmentUrl
:
attachmentUrl
+
"?thumbnail=true"
}
onError=
{
(
e
)
=>
{
// Fallback to original image if thumbnail fails
const
target
=
e
.
target
as
HTMLImageElement
;
if
(
target
.
src
.
includes
(
"?thumbnail=true"
))
{
console
.
warn
(
"Thumbnail failed, falling back to original image:"
,
attachmentUrl
);
target
.
src
=
attachmentUrl
;
}
}
}
onClick=
{
()
=>
handleImageClick
(
attachmentUrl
)
}
decoding=
"async"
loading=
"lazy"
...
...
@@ -88,6 +101,13 @@ const MemoAttachmentListView = ({ attachments = [] }: { attachments: Attachment[
<>
{
mediaAttachments
.
length
>
0
&&
<
MediaList
attachments=
{
mediaAttachments
}
/>
}
<
OtherList
attachments=
{
otherAttachments
}
/>
<
PreviewImageDialog
open=
{
previewImage
.
open
}
onOpenChange=
{
(
open
)
=>
setPreviewImage
((
prev
)
=>
({
...
prev
,
open
}))
}
imgUrls=
{
previewImage
.
urls
}
initialIndex=
{
previewImage
.
index
}
/>
</>
);
};
...
...
web/src/components/MemoView.tsx
View file @
240d89fb
...
...
@@ -20,7 +20,7 @@ import MemoEditor from "./MemoEditor";
import
MemoLocationView
from
"./MemoLocationView"
;
import
MemoReactionistView
from
"./MemoReactionListView"
;
import
MemoRelationListView
from
"./MemoRelationListView"
;
import
showPreviewImageDialog
from
"./PreviewImageDialog"
;
import
{
PreviewImageDialog
}
from
"./PreviewImageDialog"
;
import
ReactionSelector
from
"./ReactionSelector"
;
import
UserAvatar
from
"./UserAvatar"
;
import
VisibilityIcon
from
"./VisibilityIcon"
;
...
...
@@ -46,6 +46,11 @@ const MemoView: React.FC<Props> = observer((props: Props) => {
const
[
showEditor
,
setShowEditor
]
=
useState
<
boolean
>
(
false
);
const
[
creator
,
setCreator
]
=
useState
(
userStore
.
getUserByName
(
memo
.
creator
));
const
[
showNSFWContent
,
setShowNSFWContent
]
=
useState
(
props
.
showNsfwContent
);
const
[
previewImage
,
setPreviewImage
]
=
useState
<
{
open
:
boolean
;
urls
:
string
[];
index
:
number
}
>
({
open
:
false
,
urls
:
[],
index
:
0
,
});
const
workspaceMemoRelatedSetting
=
workspaceStore
.
state
.
memoRelatedSetting
;
const
referencedMemos
=
memo
.
relations
.
filter
((
relation
)
=>
relation
.
type
===
MemoRelation_Type
.
REFERENCE
);
const
commentAmount
=
memo
.
relations
.
filter
(
...
...
@@ -80,7 +85,7 @@ const MemoView: React.FC<Props> = observer((props: Props) => {
if
(
targetEl
.
tagName
===
"IMG"
)
{
const
imgUrl
=
targetEl
.
getAttribute
(
"src"
);
if
(
imgUrl
)
{
s
howPreviewImageDialog
([
imgUrl
],
0
);
s
etPreviewImage
({
open
:
true
,
urls
:
[
imgUrl
],
index
:
0
}
);
}
}
},
[]);
...
...
@@ -256,6 +261,13 @@ const MemoView: React.FC<Props> = observer((props: Props) => {
</
button
>
</>
)
}
<
PreviewImageDialog
open=
{
previewImage
.
open
}
onOpenChange=
{
(
open
)
=>
setPreviewImage
((
prev
)
=>
({
...
prev
,
open
}))
}
imgUrls=
{
previewImage
.
urls
}
initialIndex=
{
previewImage
.
index
}
/>
</
div
>
);
});
...
...
web/src/components/PreviewImageDialog.tsx
View file @
240d89fb
import
{
X
Icon
}
from
"lucide-react"
;
import
{
X
}
from
"lucide-react"
;
import
React
,
{
useEffect
,
useState
}
from
"react"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
generateDialog
}
from
"./D
ialog"
;
import
{
Dialog
,
DialogContent
}
from
"@/components/ui/d
ialog"
;
const
MIN_SCALE
=
0.5
;
const
MAX_SCALE
=
5
;
const
SCALE_UNIT
=
0.2
;
interface
Props
extends
DialogProps
{
interface
PreviewImageDialogProps
{
open
:
boolean
;
onOpenChange
:
(
open
:
boolean
)
=>
void
;
imgUrls
:
string
[];
initialIndex
:
number
;
}
interface
State
{
scale
:
number
;
originX
:
number
;
originY
:
number
;
initialIndex
?:
number
;
}
const
defaultState
:
State
=
{
scale
:
1
,
originX
:
-
1
,
originY
:
-
1
,
};
const
PreviewImageDialog
:
React
.
FC
<
Props
>
=
({
destroy
,
imgUrls
,
initialIndex
}:
Props
)
=>
{
export
function
PreviewImageDialog
({
open
,
onOpenChange
,
imgUrls
,
initialIndex
=
0
}:
PreviewImageDialogProps
)
{
const
[
currentIndex
,
setCurrentIndex
]
=
useState
(
initialIndex
);
const
[
state
,
setState
]
=
useState
<
State
>
(
defaultState
);
let
startX
=
-
1
;
let
endX
=
-
1
;
const
handleCloseBtnClick
=
()
=>
{
destroyAndResetViewport
();
};
const
handleTouchStart
=
(
event
:
React
.
TouchEvent
)
=>
{
if
(
event
.
touches
.
length
>
1
)
{
// two or more fingers, ignore
return
;
}
startX
=
event
.
touches
[
0
].
clientX
;
};
const
handleTouchMove
=
(
event
:
React
.
TouchEvent
)
=>
{
if
(
event
.
touches
.
length
>
1
)
{
// two or more fingers, ignore
return
;
}
endX
=
event
.
touches
[
0
].
clientX
;
};
// Update current index when initialIndex prop changes
useEffect
(()
=>
{
setCurrentIndex
(
initialIndex
);
},
[
initialIndex
]);
const
handleTouchEnd
=
(
event
:
React
.
TouchEvent
)
=>
{
if
(
event
.
touches
.
length
>
1
)
{
// two or more fingers, ignore
return
;
}
if
(
startX
>
-
1
&&
endX
>
-
1
)
{
const
distance
=
startX
-
endX
;
if
(
distance
>
50
)
{
showNextImg
()
;
}
else
if
(
distance
<
-
50
)
{
showPrevImg
()
;
// Handle keyboard navigation
useEffect
(()
=>
{
const
handleKeyDown
=
(
event
:
KeyboardEvent
)
=>
{
if
(
!
open
)
return
;
switch
(
event
.
key
)
{
case
"Escape"
:
onOpenChange
(
false
);
break
;
default
:
break
;
}
}
endX
=
-
1
;
startX
=
-
1
;
};
const
showPrevImg
=
()
=>
{
if
(
currentIndex
>
0
)
{
setState
(
defaultState
);
setCurrentIndex
(
currentIndex
-
1
);
}
else
{
destroyAndResetViewport
();
}
};
const
showNextImg
=
()
=>
{
if
(
currentIndex
<
imgUrls
.
length
-
1
)
{
setState
(
defaultState
);
setCurrentIndex
(
currentIndex
+
1
);
}
else
{
destroyAndResetViewport
();
}
};
};
const
handleImgContainerClick
=
(
event
:
React
.
MouseEvent
)
=>
{
if
(
event
.
clientX
<
window
.
innerWidth
/
2
)
{
showPrevImg
();
}
else
{
showNextImg
();
}
};
document
.
addEventListener
(
"keydown"
,
handleKeyDown
);
return
()
=>
document
.
removeEventListener
(
"keydown"
,
handleKeyDown
);
},
[
open
,
onOpenChange
]);
const
handleImageContainerKeyDown
=
(
event
:
KeyboardEvent
)
=>
{
switch
(
event
.
key
)
{
case
"ArrowLeft"
:
showPrevImg
();
break
;
case
"ArrowRight"
:
showNextImg
();
break
;
case
"Escape"
:
destroyAndResetViewport
();
break
;
default
:
}
const
handleClose
=
()
=>
{
onOpenChange
(
false
);
};
const
handleImgContainerScroll
=
(
event
:
React
.
WheelEvent
)
=>
{
// Prevent closing when clicking on the image
const
handleImageClick
=
(
event
:
React
.
MouseEvent
)
=>
{
event
.
stopPropagation
();
const
offsetX
=
event
.
nativeEvent
.
offsetX
;
const
offsetY
=
event
.
nativeEvent
.
offsetY
;
const
sign
=
event
.
deltaY
<
0
?
1
:
-
1
;
const
scale
=
Math
.
max
(
MIN_SCALE
,
Math
.
min
(
MAX_SCALE
,
state
.
scale
+
sign
*
SCALE_UNIT
));
setState
({
...
state
,
originX
:
offsetX
,
originY
:
offsetY
,
scale
:
scale
,
});
};
const
setViewportScalable
=
()
=>
{
const
viewport
=
document
.
querySelector
(
"meta[name=viewport]"
);
if
(
viewport
)
{
const
contentAttrs
=
viewport
.
getAttribute
(
"content"
);
if
(
contentAttrs
)
{
viewport
.
setAttribute
(
"content"
,
contentAttrs
.
replace
(
"user-scalable=no"
,
"user-scalable=yes"
));
}
}
};
const
destroyAndResetViewport
=
()
=>
{
const
viewport
=
document
.
querySelector
(
"meta[name=viewport]"
);
if
(
viewport
)
{
const
contentAttrs
=
viewport
.
getAttribute
(
"content"
);
if
(
contentAttrs
)
{
viewport
.
setAttribute
(
"content"
,
contentAttrs
.
replace
(
"user-scalable=yes"
,
"user-scalable=no"
));
}
}
destroy
();
};
const
imageComputedStyle
=
{
transform
:
`scale(
${
state
.
scale
}
)`
,
transformOrigin
:
`
${
state
.
originX
===
-
1
?
"center"
:
`
${
state
.
originX
}
px`
}
${
state
.
originY
===
-
1
?
"center"
:
`
${
state
.
originY
}
px`
}
`
,
};
useEffect
(()
=>
{
setViewportScalable
();
},
[]);
// Return early if no images provided
if
(
!
imgUrls
.
length
)
return
null
;
useEffect
(()
=>
{
document
.
addEventListener
(
"keydown"
,
handleImageContainerKeyDown
);
return
()
=>
{
document
.
removeEventListener
(
"keydown"
,
handleImageContainerKeyDown
);
};
},
[
currentIndex
]);
// Ensure currentIndex is within bounds
const
safeIndex
=
Math
.
max
(
0
,
Math
.
min
(
currentIndex
,
imgUrls
.
length
-
1
));
return
(
<>
<
div
className=
"fixed top-8 right-8 z-1 flex flex-col justify-start items-center"
>
<
Button
onClick=
{
handleCloseBtnClick
}
>
<
XIcon
className=
"w-6 h-auto"
/>
</
Button
>
</
div
>
<
div
className=
"w-full h-screen p-4 sm:p-8 flex flex-col justify-center items-center hide-scrollbar"
onClick=
{
handleImgContainerClick
}
<
Dialog
open=
{
open
}
onOpenChange=
{
onOpenChange
}
>
<
DialogContent
className=
"!w-[100vw] !h-[100vh] !max-w-[100vw] !max-h-[100vw] p-0 border-0 shadow-none bg-transparent [&>button]:hidden"
aria
-
describedby=
"image-preview-description"
>
<
img
className=
"object-contain max-h-full max-w-full"
style=
{
imageComputedStyle
}
src=
{
imgUrls
[
currentIndex
]
}
onClick=
{
(
e
)
=>
e
.
stopPropagation
()
}
onTouchStart=
{
handleTouchStart
}
onTouchMove=
{
handleTouchMove
}
onTouchEnd=
{
handleTouchEnd
}
onWheel=
{
handleImgContainerScroll
}
decoding=
"async"
loading=
"lazy"
/>
</
div
>
</>
);
};
export
default
function
showPreviewImageDialog
(
imgUrls
:
string
[]
|
string
,
initialIndex
?:
number
):
void
{
generateDialog
(
{
className
:
"preview-image-dialog p-0 z-1001"
,
dialogName
:
"preview-image-dialog"
,
},
PreviewImageDialog
,
{
imgUrls
:
Array
.
isArray
(
imgUrls
)
?
imgUrls
:
[
imgUrls
],
initialIndex
:
initialIndex
||
0
,
},
{
/* Close button */
}
<
div
className=
"fixed top-4 right-4 z-50"
>
<
Button
onClick=
{
handleClose
}
variant=
"secondary"
size=
"icon"
className=
"rounded-full bg-popover/20 hover:bg-popover/30 border-border/20 backdrop-blur-sm"
aria
-
label=
"Close image preview"
>
<
X
className=
"h-4 w-4 text-popover-foreground"
/>
</
Button
>
</
div
>
{
/* Image container */
}
<
div
className=
"w-full h-full flex items-center justify-center p-4 sm:p-8 overflow-auto"
>
<
img
src=
{
imgUrls
[
safeIndex
]
}
alt=
{
`Preview image ${safeIndex + 1} of ${imgUrls.length}`
}
className=
"max-w-full max-h-full object-contain select-none"
onClick=
{
handleImageClick
}
draggable=
{
false
}
loading=
"eager"
decoding=
"async"
/>
</
div
>
{
/* Screen reader description */
}
<
div
id=
"image-preview-description"
className=
"sr-only"
>
Image preview dialog. Press Escape to close or click outside the image.
</
div
>
</
DialogContent
>
</
Dialog
>
);
}
web/src/components/RenameTagDialog.tsx
View file @
240d89fb
import
{
XIcon
}
from
"lucide-react"
;
import
React
,
{
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Dialog
,
DialogContent
,
DialogFooter
,
DialogHeader
,
DialogTitle
}
from
"@/components/ui/dialog"
;
import
{
Input
}
from
"@/components/ui/input"
;
import
{
Label
}
from
"@/components/ui/label"
;
import
{
memoServiceClient
}
from
"@/grpcweb"
;
import
useLoading
from
"@/hooks/useLoading"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
generateDialog
}
from
"./Dialog"
;
interface
Props
extends
DialogProps
{
interface
RenameTagDialogProps
{
open
:
boolean
;
onOpenChange
:
(
open
:
boolean
)
=>
void
;
tag
:
string
;
onSuccess
?:
()
=>
void
;
}
const
RenameTagDialog
:
React
.
FC
<
Props
>
=
(
props
:
Props
)
=>
{
const
{
tag
,
destroy
}
=
props
;
export
function
RenameTagDialog
({
open
,
onOpenChange
,
tag
,
onSuccess
}:
RenameTagDialogProps
)
{
const
t
=
useTranslate
();
const
[
newName
,
setNewName
]
=
useState
(
tag
);
const
requestState
=
useLoading
(
false
);
...
...
@@ -33,65 +35,55 @@ const RenameTagDialog: React.FC<Props> = (props: Props) => {
}
try
{
requestState
.
setLoading
();
await
memoServiceClient
.
renameMemoTag
({
parent
:
"memos/-"
,
oldTag
:
tag
,
newTag
:
newName
,
});
toast
.
success
(
t
(
"tag.rename-success"
));
requestState
.
setFinish
();
onSuccess
?.();
onOpenChange
(
false
);
}
catch
(
error
:
any
)
{
console
.
error
(
error
);
toast
.
error
(
error
.
details
);
requestState
.
setError
();
}
destroy
();
};
return
(
<
div
className=
"max-w-full shadow flex flex-col justify-start items-start bg-card text-card-foreground p-4 rounded-lg"
>
<
div
className=
"flex flex-row justify-between items-center mb-4 gap-2 w-full"
>
<
p
className=
"title-text"
>
{
t
(
"tag.rename-tag"
)
}
</
p
>
<
Button
variant=
"ghost"
onClick=
{
()
=>
destroy
()
}
>
<
XIcon
className=
"w-5 h-auto"
/>
</
Button
>
</
div
>
<
div
className=
"flex flex-col justify-start items-start max-w-xs"
>
<
div
className=
"w-full flex flex-col justify-start items-start mb-3"
>
<
div
className=
"relative w-full mb-2 flex flex-row justify-start items-center space-x-2"
>
<
span
className=
"w-20 text-sm whitespace-nowrap shrink-0 text-right"
>
{
t
(
"tag.old-name"
)
}
</
span
>
<
Input
className=
"w-full"
readOnly
disabled
type=
"text"
placeholder=
"A new tag name"
value=
{
tag
}
/>
<
Dialog
open=
{
open
}
onOpenChange=
{
onOpenChange
}
>
<
DialogContent
className=
"max-w-md"
>
<
DialogHeader
>
<
DialogTitle
>
{
t
(
"tag.rename-tag"
)
}
</
DialogTitle
>
</
DialogHeader
>
<
div
className=
"flex flex-col gap-4"
>
<
div
className=
"grid gap-2"
>
<
Label
htmlFor=
"oldName"
>
{
t
(
"tag.old-name"
)
}
</
Label
>
<
Input
id=
"oldName"
readOnly
disabled
type=
"text"
value=
{
tag
}
/>
</
div
>
<
div
className=
"relative w-full mb-2 flex flex-row justify-start items-center space-x-2"
>
<
span
className=
"w-20 text-sm whitespace-nowrap shrink-0 text-right"
>
{
t
(
"tag.new-name"
)
}
</
span
>
<
Input
className=
"w-full"
type=
"text"
placeholder=
"A new tag name"
value=
{
newName
}
onChange=
{
handleTagNameInputChange
}
/>
<
div
className=
"grid gap-2"
>
<
Label
htmlFor=
"newName"
>
{
t
(
"tag.new-name"
)
}
</
Label
>
<
Input
id=
"newName"
type=
"text"
placeholder=
"A new tag name"
value=
{
newName
}
onChange=
{
handleTagNameInputChange
}
/>
</
div
>
<
div
className=
"text-sm text-muted-foreground"
>
<
ul
className=
"list-disc list-inside"
>
<
li
>
{
t
(
"tag.rename-tip"
)
}
</
li
>
</
ul
>
</
div
>
<
ul
className=
"list-disc list-inside text-sm ml-4"
>
<
li
>
<
p
className=
"leading-5"
>
{
t
(
"tag.rename-tip"
)
}
</
p
>
</
li
>
</
ul
>
</
div
>
<
div
className=
"w-full flex flex-row justify-end items-center space-x-2"
>
<
Button
variant=
"ghost"
disabled=
{
requestState
.
isLoading
}
onClick=
{
destroy
}
>
<
DialogFooter
>
<
Button
variant=
"ghost"
disabled=
{
requestState
.
isLoading
}
onClick=
{
()
=>
onOpenChange
(
false
)
}
>
{
t
(
"common.cancel"
)
}
</
Button
>
<
Button
disabled=
{
requestState
.
isLoading
}
onClick=
{
handleConfirm
}
>
{
t
(
"common.confirm"
)
}
</
Button
>
</
div
>
</
div
>
</
div
>
);
};
function
showRenameTagDialog
(
props
:
Pick
<
Props
,
"tag"
>
)
{
generateDialog
(
{
className
:
"rename-tag-dialog"
,
dialogName
:
"rename-tag-dialog"
,
},
RenameTagDialog
,
props
,
</
DialogFooter
>
</
DialogContent
>
</
Dialog
>
);
}
export
default
show
RenameTagDialog
;
export
default
RenameTagDialog
;
web/src/components/Settings/AccessTokenSection.tsx
View file @
240d89fb
...
...
@@ -5,9 +5,10 @@ import { toast } from "react-hot-toast";
import
{
Button
}
from
"@/components/ui/button"
;
import
{
userServiceClient
}
from
"@/grpcweb"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
{
useDialog
}
from
"@/hooks/useDialog"
;
import
{
UserAccessToken
}
from
"@/types/proto/api/v1/user_service"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
show
CreateAccessTokenDialog
from
"../CreateAccessTokenDialog"
;
import
CreateAccessTokenDialog
from
"../CreateAccessTokenDialog"
;
import
LearnMore
from
"../LearnMore"
;
const
listAccessTokens
=
async
(
parent
:
string
)
=>
{
...
...
@@ -19,6 +20,7 @@ const AccessTokenSection = () => {
const
t
=
useTranslate
();
const
currentUser
=
useCurrentUser
();
const
[
userAccessTokens
,
setUserAccessTokens
]
=
useState
<
UserAccessToken
[]
>
([]);
const
createTokenDialog
=
useDialog
();
useEffect
(()
=>
{
listAccessTokens
(
currentUser
.
name
).
then
((
accessTokens
)
=>
{
...
...
@@ -31,6 +33,10 @@ const AccessTokenSection = () => {
setUserAccessTokens
(
accessTokens
);
};
const
handleCreateToken
=
()
=>
{
createTokenDialog
.
open
();
};
const
copyAccessToken
=
(
accessToken
:
string
)
=>
{
copy
(
accessToken
);
toast
.
success
(
t
(
"setting.access-token-section.access-token-copied-to-clipboard"
));
...
...
@@ -61,12 +67,7 @@ const AccessTokenSection = () => {
<
p
className=
"text-sm text-muted-foreground"
>
{
t
(
"setting.access-token-section.description"
)
}
</
p
>
</
div
>
<
div
className=
"mt-4 sm:mt-0"
>
<
Button
color=
"primary"
onClick=
{
()
=>
{
showCreateAccessTokenDialog
(
handleCreateAccessTokenDialogConfirm
);
}
}
>
<
Button
color=
"primary"
onClick=
{
handleCreateToken
}
>
{
t
(
"common.create"
)
}
</
Button
>
</
div
>
...
...
@@ -128,6 +129,13 @@ const AccessTokenSection = () => {
</
div
>
</
div
>
</
div
>
{
/* Create Access Token Dialog */
}
<
CreateAccessTokenDialog
open=
{
createTokenDialog
.
isOpen
}
onOpenChange=
{
createTokenDialog
.
setOpen
}
onSuccess=
{
handleCreateAccessTokenDialogConfirm
}
/>
</
div
>
);
};
...
...
web/src/components/Settings/MemberSection.tsx
View file @
240d89fb
import
{
sortBy
}
from
"lodash-es"
;
import
{
MoreVerticalIcon
}
from
"lucide-react"
;
import
{
MoreVerticalIcon
,
PlusIcon
}
from
"lucide-react"
;
import
{
observer
}
from
"mobx-react-lite"
;
import
React
,
{
useEffect
,
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Input
}
from
"@/components/ui/input"
;
import
{
Label
}
from
"@/components/ui/label"
;
import
{
RadioGroup
,
RadioGroupItem
}
from
"@/components/ui/radio-group"
;
import
{
userServiceClient
}
from
"@/grpcweb"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
{
useDialog
}
from
"@/hooks/useDialog"
;
import
{
userStore
}
from
"@/store/v2"
;
import
{
State
}
from
"@/types/proto/api/v1/common"
;
import
{
User
,
User_Role
}
from
"@/types/proto/api/v1/user_service"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
show
CreateUserDialog
from
"../CreateUserDialog"
;
import
CreateUserDialog
from
"../CreateUserDialog"
;
import
{
DropdownMenu
,
DropdownMenuContent
,
DropdownMenuItem
,
DropdownMenuTrigger
}
from
"../ui/dropdown-menu"
;
interface
LocalState
{
creatingUser
:
User
;
}
const
MemberSection
=
observer
(()
=>
{
const
t
=
useTranslate
();
const
currentUser
=
useCurrentUser
();
const
[
state
,
setState
]
=
useState
<
LocalState
>
({
creatingUser
:
User
.
fromPartial
({
username
:
""
,
password
:
""
,
role
:
User_Role
.
USER
,
}),
});
const
[
users
,
setUsers
]
=
useState
<
User
[]
>
([]);
const
createDialog
=
useDialog
();
const
editDialog
=
useDialog
();
const
[
editingUser
,
setEditingUser
]
=
useState
<
User
|
undefined
>
();
const
sortedUsers
=
sortBy
(
users
,
"id"
);
useEffect
(()
=>
{
...
...
@@ -52,62 +41,14 @@ const MemberSection = observer(() => {
}
};
const
handleUsernameInputChange
=
(
event
:
React
.
ChangeEvent
<
HTMLInputElement
>
)
=>
{
setState
({
...
state
,
creatingUser
:
{
...
state
.
creatingUser
,
username
:
event
.
target
.
value
,
},
});
};
const
handlePasswordInputChange
=
(
event
:
React
.
ChangeEvent
<
HTMLInputElement
>
)
=>
{
setState
({
...
state
,
creatingUser
:
{
...
state
.
creatingUser
,
password
:
event
.
target
.
value
,
},
});
};
const
handleUserRoleInputChange
=
(
event
:
React
.
ChangeEvent
<
HTMLInputElement
>
)
=>
{
setState
({
...
state
,
creatingUser
:
{
...
state
.
creatingUser
,
role
:
event
.
target
.
value
as
User_Role
,
},
});
const
handleCreateUser
=
()
=>
{
setEditingUser
(
undefined
);
createDialog
.
open
();
};
const
handleCreateUserBtnClick
=
async
()
=>
{
if
(
state
.
creatingUser
.
username
===
""
||
state
.
creatingUser
.
password
===
""
)
{
toast
.
error
(
t
(
"message.fill-all"
));
return
;
}
try
{
await
userServiceClient
.
createUser
({
user
:
{
username
:
state
.
creatingUser
.
username
,
password
:
state
.
creatingUser
.
password
,
role
:
state
.
creatingUser
.
role
,
},
});
}
catch
(
error
:
any
)
{
toast
.
error
(
error
.
details
);
}
await
fetchUsers
();
setState
({
...
state
,
creatingUser
:
User
.
fromPartial
({
username
:
""
,
password
:
""
,
role
:
User_Role
.
USER
,
}),
});
const
handleEditUser
=
(
user
:
User
)
=>
{
setEditingUser
(
user
);
editDialog
.
open
();
};
const
handleArchiveUserClick
=
async
(
user
:
User
)
=>
{
...
...
@@ -145,48 +86,12 @@ const MemberSection = observer(() => {
return
(
<
div
className=
"w-full flex flex-col gap-2 pt-2 pb-4"
>
<
p
className=
"font-medium text-muted-foreground"
>
{
t
(
"setting.member-section.create-a-member"
)
}
</
p
>
<
div
className=
"w-auto flex flex-col justify-start items-start gap-2 border border-border rounded-md py-2 px-3"
>
<
div
className=
"flex flex-col justify-start items-start gap-1"
>
<
span
>
{
t
(
"common.username"
)
}
</
span
>
<
Input
type=
"text"
placeholder=
{
t
(
"common.username"
)
}
autoComplete=
"off"
value=
{
state
.
creatingUser
.
username
}
onChange=
{
handleUsernameInputChange
}
/>
</
div
>
<
div
className=
"flex flex-col justify-start items-start gap-1"
>
<
span
>
{
t
(
"common.password"
)
}
</
span
>
<
Input
type=
"password"
placeholder=
{
t
(
"common.password"
)
}
autoComplete=
"off"
value=
{
state
.
creatingUser
.
password
}
onChange=
{
handlePasswordInputChange
}
/>
</
div
>
<
div
className=
"flex flex-col justify-start items-start gap-1"
>
<
span
>
{
t
(
"common.role"
)
}
</
span
>
<
RadioGroup
defaultValue=
{
User_Role
.
USER
}
onValueChange=
{
(
value
)
=>
handleUserRoleInputChange
({
target
:
{
value
}
}
as
React
.
ChangeEvent
<
HTMLInputElement
>
)
}
className=
"flex flex-row gap-4"
>
<
div
className=
"flex items-center space-x-2"
>
<
RadioGroupItem
value=
{
User_Role
.
USER
}
id=
"user-role"
/>
<
Label
htmlFor=
"user-role"
>
{
t
(
"setting.member-section.user"
)
}
</
Label
>
</
div
>
<
div
className=
"flex items-center space-x-2"
>
<
RadioGroupItem
value=
{
User_Role
.
ADMIN
}
id=
"admin-role"
/>
<
Label
htmlFor=
"admin-role"
>
{
t
(
"setting.member-section.admin"
)
}
</
Label
>
</
div
>
</
RadioGroup
>
</
div
>
<
div
className=
"mt-2"
>
<
Button
onClick=
{
handleCreateUserBtnClick
}
>
{
t
(
"common.create"
)
}
</
Button
>
</
div
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
p
className=
"font-medium text-muted-foreground"
>
{
t
(
"setting.member-section.create-a-member"
)
}
</
p
>
<
Button
onClick=
{
handleCreateUser
}
>
<
PlusIcon
className=
"w-4 h-4 mr-2"
/>
{
t
(
"common.create"
)
}
</
Button
>
</
div
>
<
div
className=
"w-full flex flex-row justify-between items-center mt-6"
>
<
div
className=
"title-text"
>
{
t
(
"setting.member-list"
)
}
</
div
>
...
...
@@ -232,9 +137,7 @@ const MemberSection = observer(() => {
</
Button
>
</
DropdownMenuTrigger
>
<
DropdownMenuContent
align=
"end"
sideOffset=
{
2
}
>
<
DropdownMenuItem
onClick=
{
()
=>
showCreateUserDialog
(
user
,
()
=>
fetchUsers
())
}
>
{
t
(
"common.update"
)
}
</
DropdownMenuItem
>
<
DropdownMenuItem
onClick=
{
()
=>
handleEditUser
(
user
)
}
>
{
t
(
"common.update"
)
}
</
DropdownMenuItem
>
{
user
.
state
===
State
.
NORMAL
?
(
<
DropdownMenuItem
onClick=
{
()
=>
handleArchiveUserClick
(
user
)
}
>
{
t
(
"setting.member-section.archive-member"
)
}
...
...
@@ -260,6 +163,12 @@ const MemberSection = observer(() => {
</
table
>
</
div
>
</
div
>
{
/* Create User Dialog */
}
<
CreateUserDialog
open=
{
createDialog
.
isOpen
}
onOpenChange=
{
createDialog
.
setOpen
}
onSuccess=
{
fetchUsers
}
/>
{
/* Edit User Dialog */
}
<
CreateUserDialog
open=
{
editDialog
.
isOpen
}
onOpenChange=
{
editDialog
.
setOpen
}
user=
{
editingUser
}
onSuccess=
{
fetchUsers
}
/>
</
div
>
);
});
...
...
web/src/components/Settings/MyAccountSection.tsx
View file @
240d89fb
import
{
MoreVerticalIcon
,
PenLineIcon
}
from
"lucide-react"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
{
useDialog
}
from
"@/hooks/useDialog"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
show
ChangeMemberPasswordDialog
from
"../ChangeMemberPasswordDialog"
;
import
show
UpdateAccountDialog
from
"../UpdateAccountDialog"
;
import
ChangeMemberPasswordDialog
from
"../ChangeMemberPasswordDialog"
;
import
UpdateAccountDialog
from
"../UpdateAccountDialog"
;
import
UserAvatar
from
"../UserAvatar"
;
import
{
DropdownMenu
,
DropdownMenuContent
,
DropdownMenuItem
,
DropdownMenuTrigger
}
from
"../ui/dropdown-menu"
;
import
AccessTokenSection
from
"./AccessTokenSection"
;
...
...
@@ -12,6 +13,16 @@ import UserSessionsSection from "./UserSessionsSection";
const
MyAccountSection
=
()
=>
{
const
t
=
useTranslate
();
const
user
=
useCurrentUser
();
const
accountDialog
=
useDialog
();
const
passwordDialog
=
useDialog
();
const
handleEditAccount
=
()
=>
{
accountDialog
.
open
();
};
const
handleChangePassword
=
()
=>
{
passwordDialog
.
open
();
};
return
(
<
div
className=
"w-full gap-2 pt-2 pb-4"
>
...
...
@@ -27,7 +38,7 @@ const MyAccountSection = () => {
</
div
>
</
div
>
<
div
className=
"w-full flex flex-row justify-start items-center mt-2 space-x-2"
>
<
Button
variant=
"outline"
onClick=
{
showUpdateAccountDialog
}
>
<
Button
variant=
"outline"
onClick=
{
handleEditAccount
}
>
<
PenLineIcon
className=
"w-4 h-4 mx-auto mr-1"
/>
{
t
(
"common.edit"
)
}
</
Button
>
...
...
@@ -38,15 +49,19 @@ const MyAccountSection = () => {
</
Button
>
</
DropdownMenuTrigger
>
<
DropdownMenuContent
align=
"start"
>
<
DropdownMenuItem
onClick=
{
()
=>
showChangeMemberPasswordDialog
(
user
)
}
>
{
t
(
"setting.account-section.change-password"
)
}
</
DropdownMenuItem
>
<
DropdownMenuItem
onClick=
{
handleChangePassword
}
>
{
t
(
"setting.account-section.change-password"
)
}
</
DropdownMenuItem
>
</
DropdownMenuContent
>
</
DropdownMenu
>
</
div
>
<
UserSessionsSection
/>
<
AccessTokenSection
/>
{
/* Update Account Dialog */
}
<
UpdateAccountDialog
open=
{
accountDialog
.
isOpen
}
onOpenChange=
{
accountDialog
.
setOpen
}
/>
{
/* Change Password Dialog */
}
<
ChangeMemberPasswordDialog
open=
{
passwordDialog
.
isOpen
}
onOpenChange=
{
passwordDialog
.
setOpen
}
user=
{
user
}
/>
</
div
>
);
};
...
...
web/src/components/Settings/SSOSection.tsx
View file @
240d89fb
...
...
@@ -8,12 +8,14 @@ import { Separator } from "@/components/ui/separator";
import
{
identityProviderServiceClient
}
from
"@/grpcweb"
;
import
{
IdentityProvider
}
from
"@/types/proto/api/v1/idp_service"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
show
CreateIdentityProviderDialog
from
"../CreateIdentityProviderDialog"
;
import
CreateIdentityProviderDialog
from
"../CreateIdentityProviderDialog"
;
import
LearnMore
from
"../LearnMore"
;
const
SSOSection
=
()
=>
{
const
t
=
useTranslate
();
const
[
identityProviderList
,
setIdentityProviderList
]
=
useState
<
IdentityProvider
[]
>
([]);
const
[
isCreateDialogOpen
,
setIsCreateDialogOpen
]
=
useState
(
false
);
const
[
editingIdentityProvider
,
setEditingIdentityProvider
]
=
useState
<
IdentityProvider
|
undefined
>
();
useEffect
(()
=>
{
fetchIdentityProviderList
();
...
...
@@ -37,6 +39,22 @@ const SSOSection = () => {
}
};
const
handleCreateIdentityProvider
=
()
=>
{
setEditingIdentityProvider
(
undefined
);
setIsCreateDialogOpen
(
true
);
};
const
handleEditIdentityProvider
=
(
identityProvider
:
IdentityProvider
)
=>
{
setEditingIdentityProvider
(
identityProvider
);
setIsCreateDialogOpen
(
true
);
};
const
handleDialogSuccess
=
async
()
=>
{
await
fetchIdentityProviderList
();
setIsCreateDialogOpen
(
false
);
setEditingIdentityProvider
(
undefined
);
};
return
(
<
div
className=
"w-full flex flex-col gap-2 pt-2 pb-4"
>
<
div
className=
"w-full flex flex-row justify-between items-center gap-1"
>
...
...
@@ -44,7 +62,7 @@ const SSOSection = () => {
<
span
className=
"font-mono text-muted-foreground"
>
{
t
(
"setting.sso-section.sso-list"
)
}
</
span
>
<
LearnMore
url=
"https://www.usememos.com/docs/advanced-settings/sso"
/>
</
div
>
<
Button
color=
"primary"
onClick=
{
()
=>
showCreateIdentityProviderDialog
(
undefined
,
fetchIdentityProviderList
)
}
>
<
Button
color=
"primary"
onClick=
{
handleCreateIdentityProvider
}
>
{
t
(
"common.create"
)
}
</
Button
>
</
div
>
...
...
@@ -68,9 +86,7 @@ const SSOSection = () => {
</
Button
>
</
DropdownMenuTrigger
>
<
DropdownMenuContent
align=
"end"
sideOffset=
{
2
}
>
<
DropdownMenuItem
onClick=
{
()
=>
showCreateIdentityProviderDialog
(
identityProvider
,
fetchIdentityProviderList
)
}
>
{
t
(
"common.edit"
)
}
</
DropdownMenuItem
>
<
DropdownMenuItem
onClick=
{
()
=>
handleEditIdentityProvider
(
identityProvider
)
}
>
{
t
(
"common.edit"
)
}
</
DropdownMenuItem
>
<
DropdownMenuItem
onClick=
{
()
=>
handleDeleteIdentityProvider
(
identityProvider
)
}
>
{
t
(
"common.delete"
)
}
</
DropdownMenuItem
>
</
DropdownMenuContent
>
</
DropdownMenu
>
...
...
@@ -93,6 +109,12 @@ const SSOSection = () => {
</
li
>
</
ul
>
</
div
>
<
CreateIdentityProviderDialog
open=
{
isCreateDialogOpen
}
onOpenChange=
{
setIsCreateDialogOpen
}
identityProvider=
{
editingIdentityProvider
}
onSuccess=
{
handleDialogSuccess
}
/>
</
div
>
);
};
...
...
web/src/components/Settings/WebhookSection.tsx
View file @
240d89fb
...
...
@@ -6,12 +6,13 @@ import { webhookServiceClient } from "@/grpcweb";
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
{
Webhook
}
from
"@/types/proto/api/v1/webhook_service"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
show
CreateWebhookDialog
from
"../CreateWebhookDialog"
;
import
CreateWebhookDialog
from
"../CreateWebhookDialog"
;
const
WebhookSection
=
()
=>
{
const
t
=
useTranslate
();
const
currentUser
=
useCurrentUser
();
const
[
webhooks
,
setWebhooks
]
=
useState
<
Webhook
[]
>
([]);
const
[
isCreateWebhookDialogOpen
,
setIsCreateWebhookDialogOpen
]
=
useState
(
false
);
const
listWebhooks
=
async
()
=>
{
if
(
!
currentUser
)
return
[];
...
...
@@ -30,6 +31,7 @@ const WebhookSection = () => {
const
handleCreateWebhookDialogConfirm
=
async
()
=>
{
const
webhooks
=
await
listWebhooks
();
setWebhooks
(
webhooks
);
setIsCreateWebhookDialogOpen
(
false
);
};
const
handleDeleteWebhook
=
async
(
webhook
:
Webhook
)
=>
{
...
...
@@ -47,12 +49,7 @@ const WebhookSection = () => {
<
p
className=
"flex flex-row justify-start items-center font-medium text-muted-foreground"
>
{
t
(
"setting.webhook-section.title"
)
}
</
p
>
</
div
>
<
div
>
<
Button
color=
"primary"
onClick=
{
()
=>
{
showCreateWebhookDialog
(
handleCreateWebhookDialogConfirm
);
}
}
>
<
Button
color=
"primary"
onClick=
{
()
=>
setIsCreateWebhookDialogOpen
(
true
)
}
>
{
t
(
"common.create"
)
}
</
Button
>
</
div
>
...
...
@@ -116,6 +113,11 @@ const WebhookSection = () => {
<
ExternalLinkIcon
className=
"inline w-4 h-auto ml-1"
/>
</
Link
>
</
div
>
<
CreateWebhookDialog
open=
{
isCreateWebhookDialogOpen
}
onOpenChange=
{
setIsCreateWebhookDialogOpen
}
onSuccess=
{
handleCreateWebhookDialogConfirm
}
/>
</
div
>
);
};
...
...
web/src/components/Settings/WorkspaceSection.tsx
View file @
240d89fb
...
...
@@ -10,16 +10,18 @@ import { Separator } from "@/components/ui/separator";
import
{
Switch
}
from
"@/components/ui/switch"
;
import
{
Textarea
}
from
"@/components/ui/textarea"
;
import
{
identityProviderServiceClient
}
from
"@/grpcweb"
;
import
useDialog
from
"@/hooks/useDialog"
;
import
{
workspaceSettingNamePrefix
}
from
"@/store/common"
;
import
{
workspaceStore
}
from
"@/store/v2"
;
import
{
WorkspaceSettingKey
}
from
"@/store/v2/workspace"
;
import
{
IdentityProvider
}
from
"@/types/proto/api/v1/idp_service"
;
import
{
WorkspaceGeneralSetting
}
from
"@/types/proto/api/v1/workspace_service"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
show
UpdateCustomizedProfileDialog
from
"../UpdateCustomizedProfileDialog"
;
import
UpdateCustomizedProfileDialog
from
"../UpdateCustomizedProfileDialog"
;
const
WorkspaceSection
=
observer
(()
=>
{
const
t
=
useTranslate
();
const
customizeDialog
=
useDialog
();
const
originalSetting
=
WorkspaceGeneralSetting
.
fromPartial
(
workspaceStore
.
getWorkspaceSettingByKey
(
WorkspaceSettingKey
.
GENERAL
)?.
generalSetting
||
{},
);
...
...
@@ -31,7 +33,7 @@ const WorkspaceSection = observer(() => {
},
[
workspaceStore
.
getWorkspaceSettingByKey
(
WorkspaceSettingKey
.
GENERAL
)]);
const
handleUpdateCustomizedProfileButtonClick
=
()
=>
{
showUpdateCustomizedProfileDialog
();
customizeDialog
.
open
();
};
const
updatePartialSetting
=
(
partial
:
Partial
<
WorkspaceGeneralSetting
>
)
=>
{
...
...
@@ -166,6 +168,15 @@ const WorkspaceSection = observer(() => {
{
t
(
"common.save"
)
}
</
Button
>
</
div
>
<
UpdateCustomizedProfileDialog
open=
{
customizeDialog
.
isOpen
}
onOpenChange=
{
customizeDialog
.
setOpen
}
onSuccess=
{
()
=>
{
// Refresh workspace settings if needed
toast
.
success
(
"Profile updated successfully!"
);
}
}
/>
</
div
>
);
});
...
...
web/src/components/UpdateAccountDialog.tsx
View file @
240d89fb
...
...
@@ -3,17 +3,22 @@ import { XIcon } from "lucide-react";
import
{
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Dialog
,
DialogContent
,
DialogFooter
,
DialogHeader
,
DialogTitle
}
from
"@/components/ui/dialog"
;
import
{
Input
}
from
"@/components/ui/input"
;
import
{
Label
}
from
"@/components/ui/label"
;
import
{
Textarea
}
from
"@/components/ui/textarea"
;
import
{
convertFileToBase64
}
from
"@/helpers/utils"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
{
userStore
,
workspaceStore
}
from
"@/store/v2"
;
import
{
User
as
UserPb
}
from
"@/types/proto/api/v1/user_service"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
generateDialog
}
from
"./Dialog"
;
import
UserAvatar
from
"./UserAvatar"
;
type
Props
=
DialogProps
;
interface
UpdateAccountDialogProps
{
open
:
boolean
;
onOpenChange
:
(
open
:
boolean
)
=>
void
;
onSuccess
?:
()
=>
void
;
}
interface
State
{
avatarUrl
:
string
;
...
...
@@ -23,7 +28,7 @@ interface State {
description
:
string
;
}
const
UpdateAccountDialog
=
({
destroy
}:
Props
)
=>
{
export
function
UpdateAccountDialog
({
open
,
onOpenChange
,
onSuccess
}:
UpdateAccountDialogProps
)
{
const
t
=
useTranslate
();
const
currentUser
=
useCurrentUser
();
const
[
state
,
setState
]
=
useState
<
State
>
({
...
...
@@ -36,7 +41,7 @@ const UpdateAccountDialog = ({ destroy }: Props) => {
const
workspaceGeneralSetting
=
workspaceStore
.
state
.
generalSetting
;
const
handleCloseBtnClick
=
()
=>
{
destroy
(
);
onOpenChange
(
false
);
};
const
setPartialState
=
(
partialState
:
Partial
<
State
>
)
=>
{
...
...
@@ -133,7 +138,8 @@ const UpdateAccountDialog = ({ destroy }: Props) => {
updateMask
,
);
toast
.
success
(
t
(
"message.update-succeed"
));
handleCloseBtnClick
();
onSuccess
?.();
onOpenChange
(
false
);
}
catch
(
error
:
any
)
{
console
.
error
(
error
);
toast
.
error
(
error
.
details
);
...
...
@@ -141,77 +147,74 @@ const UpdateAccountDialog = ({ destroy }: Props) => {
};
return
(
<
div
className=
"max-w-full shadow flex flex-col justify-start items-start bg-card text-card-foreground p-4 rounded-lg"
>
<
div
className=
"flex flex-row justify-between items-center mb-4 gap-2 w-full"
>
<
p
className=
"title-text"
>
{
t
(
"setting.account-section.update-information"
)
}
</
p
>
<
Button
variant=
"ghost"
onClick=
{
handleCloseBtnClick
}
>
<
XIcon
className=
"w-5 h-auto"
/>
</
Button
>
</
div
>
<
div
className=
"flex flex-col justify-start items-start w-64! space-y-2"
>
<
div
className=
"w-full flex flex-row justify-start items-center"
>
<
span
className=
"text-sm mr-2"
>
{
t
(
"common.avatar"
)
}
</
span
>
<
label
className=
"relative cursor-pointer hover:opacity-80"
>
<
UserAvatar
className=
"w-10! h-10!"
avatarUrl=
{
state
.
avatarUrl
}
/>
<
input
type=
"file"
accept=
"image/*"
className=
"absolute invisible w-full h-full inset-0"
onChange=
{
handleAvatarChanged
}
/>
</
label
>
{
state
.
avatarUrl
&&
(
<
XIcon
className=
"w-4 h-auto ml-1 cursor-pointer opacity-60 hover:opacity-80"
onClick=
{
()
=>
setPartialState
({
avatarUrl
:
""
,
})
}
<
Dialog
open=
{
open
}
onOpenChange=
{
onOpenChange
}
>
<
DialogContent
className=
"max-w-md"
>
<
DialogHeader
>
<
DialogTitle
>
{
t
(
"setting.account-section.update-information"
)
}
</
DialogTitle
>
</
DialogHeader
>
<
div
className=
"flex flex-col gap-4"
>
<
div
className=
"flex flex-row items-center gap-2"
>
<
Label
>
{
t
(
"common.avatar"
)
}
</
Label
>
<
label
className=
"relative cursor-pointer hover:opacity-80"
>
<
UserAvatar
className=
"w-10 h-10"
avatarUrl=
{
state
.
avatarUrl
}
/>
<
input
type=
"file"
accept=
"image/*"
className=
"absolute invisible w-full h-full inset-0"
onChange=
{
handleAvatarChanged
}
/>
</
label
>
{
state
.
avatarUrl
&&
(
<
XIcon
className=
"w-4 h-auto cursor-pointer opacity-60 hover:opacity-80"
onClick=
{
()
=>
setPartialState
({
avatarUrl
:
""
,
})
}
/>
)
}
</
div
>
<
div
className=
"grid gap-2"
>
<
Label
htmlFor=
"username"
>
{
t
(
"common.username"
)
}
<
span
className=
"text-sm text-muted-foreground ml-1"
>
(
{
t
(
"setting.account-section.username-note"
)
}
)
</
span
>
</
Label
>
<
Input
id=
"username"
value=
{
state
.
username
}
onChange=
{
handleUsernameChanged
}
disabled=
{
workspaceGeneralSetting
.
disallowChangeUsername
}
/>
</
div
>
<
div
className=
"grid gap-2"
>
<
Label
htmlFor=
"displayName"
>
{
t
(
"common.nickname"
)
}
<
span
className=
"text-sm text-muted-foreground ml-1"
>
(
{
t
(
"setting.account-section.nickname-note"
)
}
)
</
span
>
</
Label
>
<
Input
id=
"displayName"
value=
{
state
.
displayName
}
onChange=
{
handleDisplayNameChanged
}
disabled=
{
workspaceGeneralSetting
.
disallowChangeNickname
}
/>
)
}
</
div
>
<
div
className=
"grid gap-2"
>
<
Label
htmlFor=
"email"
>
{
t
(
"common.email"
)
}
<
span
className=
"text-sm text-muted-foreground ml-1"
>
(
{
t
(
"setting.account-section.email-note"
)
}
)
</
span
>
</
Label
>
<
Input
id=
"email"
type=
"email"
value=
{
state
.
email
}
onChange=
{
handleEmailChanged
}
/>
</
div
>
<
div
className=
"grid gap-2"
>
<
Label
htmlFor=
"description"
>
{
t
(
"common.description"
)
}
</
Label
>
<
Textarea
id=
"description"
rows=
{
2
}
value=
{
state
.
description
}
onChange=
{
handleDescriptionChanged
}
/>
</
div
>
</
div
>
<
p
className=
"text-sm"
>
{
t
(
"common.username"
)
}
<
span
className=
"text-sm text-muted-foreground ml-1"
>
(
{
t
(
"setting.account-section.username-note"
)
}
)
</
span
>
</
p
>
<
Input
className=
"w-full"
value=
{
state
.
username
}
onChange=
{
handleUsernameChanged
}
disabled=
{
workspaceGeneralSetting
.
disallowChangeUsername
}
/>
<
p
className=
"text-sm"
>
{
t
(
"common.nickname"
)
}
<
span
className=
"text-sm text-muted-foreground ml-1"
>
(
{
t
(
"setting.account-section.nickname-note"
)
}
)
</
span
>
</
p
>
<
Input
className=
"w-full"
value=
{
state
.
displayName
}
onChange=
{
handleDisplayNameChanged
}
disabled=
{
workspaceGeneralSetting
.
disallowChangeNickname
}
/>
<
p
className=
"text-sm"
>
{
t
(
"common.email"
)
}
<
span
className=
"text-sm text-muted-foreground ml-1"
>
(
{
t
(
"setting.account-section.email-note"
)
}
)
</
span
>
</
p
>
<
Input
className=
"w-full"
type=
"email"
value=
{
state
.
email
}
onChange=
{
handleEmailChanged
}
/>
<
p
className=
"text-sm"
>
{
t
(
"common.description"
)
}
</
p
>
<
Textarea
className=
"w-full"
rows=
{
2
}
value=
{
state
.
description
}
onChange=
{
handleDescriptionChanged
}
/>
<
div
className=
"w-full flex flex-row justify-end items-center pt-4 space-x-2"
>
<
DialogFooter
>
<
Button
variant=
"ghost"
onClick=
{
handleCloseBtnClick
}
>
{
t
(
"common.cancel"
)
}
</
Button
>
<
Button
onClick=
{
handleSaveBtnClick
}
>
{
t
(
"common.save"
)
}
</
Button
>
</
div
>
</
div
>
</
div
>
);
};
function
showUpdateAccountDialog
()
{
generateDialog
(
{
className
:
"update-account-dialog"
,
dialogName
:
"update-account-dialog"
,
},
UpdateAccountDialog
,
</
DialogFooter
>
</
DialogContent
>
</
Dialog
>
);
}
export
default
show
UpdateAccountDialog
;
export
default
UpdateAccountDialog
;
web/src/components/UpdateCustomizedProfileDialog.new.tsx
0 → 100644
View file @
240d89fb
web/src/components/UpdateCustomizedProfileDialog.tsx
View file @
240d89fb
import
{
XIcon
}
from
"lucide-react"
;
import
{
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Dialog
,
DialogContent
,
DialogDescription
,
DialogHeader
,
DialogTitle
}
from
"@/components/ui/dialog"
;
import
{
Input
}
from
"@/components/ui/input"
;
import
{
Label
}
from
"@/components/ui/label"
;
import
{
Textarea
}
from
"@/components/ui/textarea"
;
import
{
workspaceSettingNamePrefix
}
from
"@/store/common"
;
import
{
workspaceStore
}
from
"@/store/v2"
;
...
...
@@ -10,29 +11,28 @@ import { WorkspaceSettingKey } from "@/store/v2/workspace";
import
{
WorkspaceCustomProfile
}
from
"@/types/proto/api/v1/workspace_service"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
AppearanceSelect
from
"./AppearanceSelect"
;
import
{
generateDialog
}
from
"./Dialog"
;
import
LocaleSelect
from
"./LocaleSelect"
;
type
Props
=
DialogProps
;
interface
UpdateCustomizedProfileDialogProps
{
open
:
boolean
;
onOpenChange
:
(
open
:
boolean
)
=>
void
;
onSuccess
?:
()
=>
void
;
}
const
UpdateCustomizedProfileDialog
=
({
destroy
}:
Props
)
=>
{
export
function
UpdateCustomizedProfileDialog
({
open
,
onOpenChange
,
onSuccess
}:
UpdateCustomizedProfileDialogProps
)
{
const
t
=
useTranslate
();
const
workspaceGeneralSetting
=
workspaceStore
.
state
.
generalSetting
;
const
[
customProfile
,
setCustomProfile
]
=
useState
<
WorkspaceCustomProfile
>
(
WorkspaceCustomProfile
.
fromPartial
(
workspaceGeneralSetting
.
customProfile
||
{}),
);
const
handleCloseButtonClick
=
()
=>
{
destroy
();
};
const
[
isLoading
,
setIsLoading
]
=
useState
(
false
);
const
setPartialState
=
(
partialState
:
Partial
<
WorkspaceCustomProfile
>
)
=>
{
setCustomProfile
((
state
)
=>
{
return
{
...
state
,
...
partialState
,
};
});
setCustomProfile
((
state
)
=>
({
...
state
,
...
partialState
,
}));
};
const
handleNameChanged
=
(
e
:
React
.
ChangeEvent
<
HTMLInputElement
>
)
=>
{
...
...
@@ -75,12 +75,17 @@ const UpdateCustomizedProfileDialog = ({ destroy }: Props) => {
});
};
const
handleCloseButtonClick
=
()
=>
{
onOpenChange
(
false
);
};
const
handleSaveButtonClick
=
async
()
=>
{
if
(
customProfile
.
title
===
""
)
{
toast
.
error
(
"Title cannot be empty."
);
return
;
}
setIsLoading
(
true
);
try
{
await
workspaceStore
.
upsertWorkspaceSetting
({
name
:
`
${
workspaceSettingNamePrefix
}${
WorkspaceSettingKey
.
GENERAL
}
`
,
...
...
@@ -89,61 +94,75 @@ const UpdateCustomizedProfileDialog = ({ destroy }: Props) => {
customProfile
:
customProfile
,
},
});
toast
.
success
(
t
(
"message.update-succeed"
));
onSuccess
?.();
onOpenChange
(
false
);
}
catch
(
error
)
{
console
.
error
(
error
);
return
;
toast
.
error
(
"Failed to update profile"
);
}
finally
{
setIsLoading
(
false
);
}
toast
.
success
(
t
(
"message.update-succeed"
));
destroy
();
};
return
(
<
div
className=
"max-w-full shadow flex flex-col justify-start items-start bg-card text-card-foreground p-4 rounded-lg"
>
<
div
className=
"flex flex-row justify-between items-center mb-4 gap-2 w-full"
>
<
p
className=
"title-text"
>
{
t
(
"setting.system-section.customize-server.title"
)
}
</
p
>
<
Button
variant=
"ghost"
onClick=
{
handleCloseButtonClick
}
>
<
XIcon
className=
"w-5 h-auto"
/>
</
Button
>
</
div
>
<
div
className=
"flex flex-col justify-start items-start min-w-[16rem]"
>
<
p
className=
"text-sm mb-1"
>
{
t
(
"setting.system-section.server-name"
)
}
</
p
>
<
Input
className=
"w-full"
type=
"text"
value=
{
customProfile
.
title
}
onChange=
{
handleNameChanged
}
/>
<
p
className=
"text-sm mb-1 mt-2"
>
{
t
(
"setting.system-section.customize-server.icon-url"
)
}
</
p
>
<
Input
className=
"w-full"
type=
"text"
value=
{
customProfile
.
logoUrl
}
onChange=
{
handleLogoUrlChanged
}
/>
<
p
className=
"text-sm mb-1 mt-2"
>
{
t
(
"setting.system-section.customize-server.description"
)
}
</
p
>
<
Textarea
rows=
{
3
}
value=
{
customProfile
.
description
}
onChange=
{
handleDescriptionChanged
}
/>
<
p
className=
"text-sm mb-1 mt-2"
>
{
t
(
"setting.system-section.customize-server.locale"
)
}
</
p
>
<
LocaleSelect
className=
"w-full!"
value=
{
customProfile
.
locale
}
onChange=
{
handleLocaleSelectChange
}
/>
<
p
className=
"text-sm mb-1 mt-2"
>
{
t
(
"setting.system-section.customize-server.appearance"
)
}
</
p
>
<
AppearanceSelect
className=
"w-full!"
value=
{
customProfile
.
appearance
as
Appearance
}
onChange=
{
handleAppearanceSelectChange
}
/>
<
div
className=
"mt-4 w-full flex flex-row justify-between items-center space-x-2"
>
<
div
className=
"flex flex-row justify-start items-center"
>
<
Button
variant=
"outline"
onClick=
{
handleRestoreButtonClick
}
>
{
t
(
"common.restore"
)
}
</
Button
>
<
Dialog
open=
{
open
}
onOpenChange=
{
onOpenChange
}
>
<
DialogContent
className=
"max-w-2xl"
>
<
DialogHeader
>
<
DialogTitle
>
{
t
(
"setting.system-section.customize-server.title"
)
}
</
DialogTitle
>
<
DialogDescription
>
Customize your workspace appearance and settings.
</
DialogDescription
>
</
DialogHeader
>
<
div
className=
"grid gap-4 py-4"
>
<
div
className=
"grid gap-2"
>
<
Label
htmlFor=
"server-name"
>
{
t
(
"setting.system-section.server-name"
)
}
</
Label
>
<
Input
id=
"server-name"
type=
"text"
value=
{
customProfile
.
title
}
onChange=
{
handleNameChanged
}
placeholder=
"Enter server name"
/>
</
div
>
<
div
className=
"grid gap-2"
>
<
Label
htmlFor=
"icon-url"
>
{
t
(
"setting.system-section.customize-server.icon-url"
)
}
</
Label
>
<
Input
id=
"icon-url"
type=
"text"
value=
{
customProfile
.
logoUrl
}
onChange=
{
handleLogoUrlChanged
}
placeholder=
"Enter icon URL"
/>
</
div
>
<
div
className=
"grid gap-2"
>
<
Label
htmlFor=
"description"
>
{
t
(
"setting.system-section.customize-server.description"
)
}
</
Label
>
<
Textarea
id=
"description"
rows=
{
3
}
value=
{
customProfile
.
description
}
onChange=
{
handleDescriptionChanged
}
placeholder=
"Enter description"
/>
</
div
>
<
div
className=
"flex flex-row justify-end items-center gap-2"
>
<
Button
variant=
"ghost"
onClick=
{
handleCloseButtonClick
}
>
<
div
className=
"grid gap-2"
>
<
Label
>
{
t
(
"setting.system-section.customize-server.locale"
)
}
</
Label
>
<
LocaleSelect
className=
"w-full"
value=
{
customProfile
.
locale
}
onChange=
{
handleLocaleSelectChange
}
/>
</
div
>
<
div
className=
"grid gap-2"
>
<
Label
>
{
t
(
"setting.system-section.customize-server.appearance"
)
}
</
Label
>
<
AppearanceSelect
className=
"w-full"
value=
{
customProfile
.
appearance
as
Appearance
}
onChange=
{
handleAppearanceSelectChange
}
/>
</
div
>
</
div
>
<
div
className=
"flex items-center justify-between pt-4"
>
<
Button
variant=
"outline"
onClick=
{
handleRestoreButtonClick
}
disabled=
{
isLoading
}
>
{
t
(
"common.restore"
)
}
</
Button
>
<
div
className=
"flex gap-2"
>
<
Button
variant=
"ghost"
onClick=
{
handleCloseButtonClick
}
disabled=
{
isLoading
}
>
{
t
(
"common.cancel"
)
}
</
Button
>
<
Button
color=
"primary"
onClick=
{
handleSaveButtonClick
}
>
{
t
(
"common.save"
)
}
<
Button
onClick=
{
handleSaveButtonClick
}
disabled=
{
isLoading
}
>
{
isLoading
?
"Saving..."
:
t
(
"common.save"
)
}
</
Button
>
</
div
>
</
div
>
</
div
>
</
div
>
);
};
function
showUpdateCustomizedProfileDialog
()
{
generateDialog
(
{
className
:
"update-customized-profile-dialog"
,
dialogName
:
"update-customized-profile-dialog"
,
},
UpdateCustomizedProfileDialog
,
</
DialogContent
>
</
Dialog
>
);
}
export
default
show
UpdateCustomizedProfileDialog
;
export
default
UpdateCustomizedProfileDialog
;
web/src/components/examples/WorkspaceSection.example.tsx
0 → 100644
View file @
240d89fb
web/src/components/ui/dialog.tsx
View file @
240d89fb
import
*
as
DialogPrimitive
from
"@radix-ui/react-dialog"
;
import
{
cva
,
type
VariantProps
}
from
"class-variance-authority"
;
import
{
XIcon
}
from
"lucide-react"
;
import
*
as
React
from
"react"
;
import
{
cn
}
from
"@/lib/utils"
;
function
Dialog
({
...
props
}:
React
.
ComponentProps
<
typeof
DialogPrimitive
.
Root
>
)
{
return
<
DialogPrimitive
.
Root
data
-
slot=
"dialog"
{
...
props
}
/>;
}
const
Dialog
=
DialogPrimitive
.
Root
;
function
DialogTrigger
({
...
props
}:
React
.
ComponentProps
<
typeof
DialogPrimitive
.
Trigger
>
)
{
return
<
DialogPrimitive
.
Trigger
data
-
slot=
"dialog-trigger"
{
...
props
}
/>;
}
const
DialogTrigger
=
DialogPrimitive
.
Trigger
;
function
DialogPortal
({
...
props
}:
React
.
ComponentProps
<
typeof
DialogPrimitive
.
Portal
>
)
{
return
<
DialogPrimitive
.
Portal
data
-
slot=
"dialog-portal"
{
...
props
}
/>;
}
const
DialogPortal
=
DialogPrimitive
.
Portal
;
function
DialogClose
({
...
props
}:
React
.
ComponentProps
<
typeof
DialogPrimitive
.
Close
>
)
{
return
<
DialogPrimitive
.
Close
data
-
slot=
"dialog-close"
{
...
props
}
/>;
}
const
DialogClose
=
DialogPrimitive
.
Close
;
function
DialogOverlay
({
className
,
...
props
}:
React
.
ComponentProps
<
typeof
DialogPrimitive
.
Overlay
>
)
{
return
(
<
DialogPrimitive
.
Overlay
data
-
slot=
"dialog-overlay"
className=
{
cn
(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-foreground/50"
,
className
,
)
}
{
...
props
}
/>
);
}
const
DialogOverlay
=
React
.
forwardRef
<
React
.
ElementRef
<
typeof
DialogPrimitive
.
Overlay
>
,
React
.
ComponentPropsWithoutRef
<
typeof
DialogPrimitive
.
Overlay
>
>
(({
className
,
...
props
},
ref
)
=>
(
<
DialogPrimitive
.
Overlay
ref=
{
ref
}
data
-
slot=
"dialog-overlay"
className=
{
cn
(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-foreground/50"
,
className
,
)
}
{
...
props
}
/>
));
DialogOverlay
.
displayName
=
DialogPrimitive
.
Overlay
.
displayName
;
/**
* Dialog content variants with improved mobile responsiveness.
*
* Mobile behavior:
* - Mobile phones (< 640px): Uses calc(100% - 2rem) width with better 1rem margin on each side
* - Small tablets (≥ 640px): Uses calc(100% - 3rem) width with 1.5rem margin on each side
* - Medium screens and up (≥ 768px): Uses fixed max-widths based on size variant
*
* Size variants:
* - sm: max-w-sm (384px) for compact dialogs
* - default: max-w-md (448px) for standard dialogs
* - lg: max-w-lg (512px) for larger forms
* - xl: max-w-xl (576px) for detailed content
* - 2xl: max-w-2xl (672px) for wide layouts
* - full: Takes available width with margins
*/
const
dialogContentVariants
=
cva
(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border shadow-lg duration-200"
,
{
variants
:
{
size
:
{
sm
:
"w-[calc(100%-2rem)] max-w-[calc(100%-2rem)] p-4 sm:w-[calc(100%-3rem)] sm:max-w-[calc(100%-3rem)] sm:p-6 md:w-full md:max-w-sm"
,
default
:
"w-[calc(100%-2rem)] max-w-[calc(100%-2rem)] p-4 sm:w-[calc(100%-3rem)] sm:max-w-[calc(100%-3rem)] sm:p-6 md:w-full md:max-w-md"
,
lg
:
"w-[calc(100%-2rem)] max-w-[calc(100%-2rem)] p-4 sm:w-[calc(100%-3rem)] sm:max-w-[calc(100%-3rem)] sm:p-6 md:w-full md:max-w-lg"
,
xl
:
"w-[calc(100%-2rem)] max-w-[calc(100%-2rem)] p-4 sm:w-[calc(100%-3rem)] sm:max-w-[calc(100%-3rem)] sm:p-6 md:w-full md:max-w-xl"
,
"2xl"
:
"w-[calc(100%-2rem)] max-w-[calc(100%-2rem)] p-4 sm:w-[calc(100%-3rem)] sm:max-w-[calc(100%-3rem)] sm:p-6 md:w-full md:max-w-2xl"
,
full
:
"w-[calc(100%-2rem)] max-w-[calc(100%-2rem)] p-4 sm:w-[calc(100%-3rem)] sm:max-w-[calc(100%-3rem)] sm:p-6 md:w-[calc(100%-2rem)] md:max-w-none"
,
},
},
defaultVariants
:
{
size
:
"default"
,
},
},
);
function
DialogContent
({
className
,
children
,
showCloseButton
=
true
,
...
props
}:
React
.
ComponentProps
<
typeof
DialogPrimitive
.
Content
>
&
{
showCloseButton
?:
boolean
;
})
{
return
(
<
DialogPortal
data
-
slot=
"dialog-portal"
>
<
DialogOverlay
/>
<
DialogPrimitive
.
Content
data
-
slot=
"dialog-content"
className=
{
cn
(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg"
,
className
,
)
}
{
...
props
}
>
{
children
}
{
showCloseButton
&&
(
<
DialogPrimitive
.
Close
data
-
slot=
"dialog-close"
className=
"ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<
XIcon
/>
<
span
className=
"sr-only"
>
Close
</
span
>
</
DialogPrimitive
.
Close
>
)
}
</
DialogPrimitive
.
Content
>
</
DialogPortal
>
);
}
const
DialogContent
=
React
.
forwardRef
<
React
.
ElementRef
<
typeof
DialogPrimitive
.
Content
>
,
React
.
ComponentPropsWithoutRef
<
typeof
DialogPrimitive
.
Content
>
&
VariantProps
<
typeof
dialogContentVariants
>
&
{
showCloseButton
?:
boolean
;
}
>
(({
className
,
children
,
showCloseButton
=
true
,
size
,
...
props
},
ref
)
=>
(
<
DialogPortal
>
<
DialogOverlay
/>
<
DialogPrimitive
.
Content
ref=
{
ref
}
className=
{
cn
(
dialogContentVariants
({
size
}),
className
)
}
{
...
props
}
>
{
children
}
{
showCloseButton
&&
(
<
DialogPrimitive
.
Close
className=
"ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<
XIcon
/>
<
span
className=
"sr-only"
>
Close
</
span
>
</
DialogPrimitive
.
Close
>
)
}
</
DialogPrimitive
.
Content
>
</
DialogPortal
>
));
DialogContent
.
displayName
=
DialogPrimitive
.
Content
.
displayName
;
function
DialogHeader
({
className
,
...
props
}:
React
.
ComponentProps
<
"div"
>
)
{
return
<
div
data
-
slot=
"dialog-header"
className=
{
cn
(
"flex flex-col gap-2 text-center sm:text-left"
,
className
)
}
{
...
props
}
/>;
}
const
DialogHeader
=
React
.
forwardRef
<
React
.
ElementRef
<
"div"
>
,
React
.
ComponentPropsWithoutRef
<
"div"
>>
(({
className
,
...
props
},
ref
)
=>
(
<
div
ref=
{
ref
}
className=
{
cn
(
"flex flex-col gap-2 text-center sm:text-left"
,
className
)
}
{
...
props
}
/>
));
DialogHeader
.
displayName
=
"DialogHeader"
;
function
DialogFooter
({
className
,
...
props
}:
React
.
ComponentProps
<
"div"
>
)
{
return
<
div
data
-
slot=
"dialog-footer"
className=
{
cn
(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end"
,
className
)
}
{
...
props
}
/>;
}
const
DialogFooter
=
React
.
forwardRef
<
React
.
ElementRef
<
"div"
>
,
React
.
ComponentPropsWithoutRef
<
"div"
>>
(({
className
,
...
props
},
ref
)
=>
(
<
div
ref=
{
ref
}
className=
{
cn
(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end"
,
className
)
}
{
...
props
}
/>
));
DialogFooter
.
displayName
=
"DialogFooter"
;
function
DialogTitle
({
className
,
...
props
}:
React
.
ComponentProps
<
typeof
DialogPrimitive
.
Title
>
)
{
return
<
DialogPrimitive
.
Title
data
-
slot=
"dialog-title"
className=
{
cn
(
"text-lg leading-none font-semibold"
,
className
)
}
{
...
props
}
/>;
}
const
DialogTitle
=
React
.
forwardRef
<
React
.
ElementRef
<
typeof
DialogPrimitive
.
Title
>
,
React
.
ComponentPropsWithoutRef
<
typeof
DialogPrimitive
.
Title
>
>
(({
className
,
...
props
},
ref
)
=>
(
<
DialogPrimitive
.
Title
ref=
{
ref
}
className=
{
cn
(
"text-lg leading-none font-semibold"
,
className
)
}
{
...
props
}
/>
));
DialogTitle
.
displayName
=
DialogPrimitive
.
Title
.
displayName
;
function
DialogDescription
({
className
,
...
props
}:
React
.
ComponentProps
<
typeof
DialogPrimitive
.
Description
>
)
{
return
(
<
DialogPrimitive
.
Description
data
-
slot=
"dialog-description"
className=
{
cn
(
"text-muted-foreground text-sm"
,
className
)
}
{
...
props
}
/>
);
}
const
DialogDescription
=
React
.
forwardRef
<
React
.
ElementRef
<
typeof
DialogPrimitive
.
Description
>
,
React
.
ComponentPropsWithoutRef
<
typeof
DialogPrimitive
.
Description
>
>
(({
className
,
...
props
},
ref
)
=>
(
<
DialogPrimitive
.
Description
ref=
{
ref
}
className=
{
cn
(
"text-muted-foreground text-sm"
,
className
)
}
{
...
props
}
/>
));
DialogDescription
.
displayName
=
DialogPrimitive
.
Description
.
displayName
;
export
{
Dialog
,
...
...
web/src/hooks/useDialog.ts
0 → 100644
View file @
240d89fb
import
{
useState
,
useCallback
}
from
"react"
;
/**
* Hook for managing dialog state with a clean API
*
* @returns Object with dialog state and handlers
*
* @example
* const dialog = useDialog();
*
* return (
* <>
* <Button onClick={dialog.open}>Open Dialog</Button>
* <SomeDialog
* open={dialog.isOpen}
* onOpenChange={dialog.setOpen}
* onSuccess={dialog.close}
* />
* </>
* );
*/
export
function
useDialog
(
defaultOpen
=
false
)
{
const
[
isOpen
,
setIsOpen
]
=
useState
(
defaultOpen
);
const
open
=
useCallback
(()
=>
setIsOpen
(
true
),
[]);
const
close
=
useCallback
(()
=>
setIsOpen
(
false
),
[]);
const
toggle
=
useCallback
(()
=>
setIsOpen
((
prev
)
=>
!
prev
),
[]);
return
{
isOpen
,
open
,
close
,
toggle
,
setOpen
:
setIsOpen
,
};
}
/**
* Hook for managing multiple dialogs with named keys
*
* @returns Object with dialog management functions
*
* @example
* const dialogs = useDialogs();
*
* return (
* <>
* <Button onClick={() => dialogs.open('create')}>Create User</Button>
* <Button onClick={() => dialogs.open('edit')}>Edit User</Button>
*
* <CreateUserDialog
* open={dialogs.isOpen('create')}
* onOpenChange={(open) => dialogs.setOpen('create', open)}
* />
* <EditUserDialog
* open={dialogs.isOpen('edit')}
* onOpenChange={(open) => dialogs.setOpen('edit', open)}
* />
* </>
* );
*/
export
function
useDialogs
()
{
const
[
openDialogs
,
setOpenDialogs
]
=
useState
<
Set
<
string
>>
(
new
Set
());
const
isOpen
=
useCallback
((
key
:
string
)
=>
openDialogs
.
has
(
key
),
[
openDialogs
]);
const
open
=
useCallback
((
key
:
string
)
=>
{
setOpenDialogs
((
prev
)
=>
new
Set
([...
prev
,
key
]));
},
[]);
const
close
=
useCallback
((
key
:
string
)
=>
{
setOpenDialogs
((
prev
)
=>
{
const
next
=
new
Set
(
prev
);
next
.
delete
(
key
);
return
next
;
});
},
[]);
const
toggle
=
useCallback
((
key
:
string
)
=>
{
setOpenDialogs
((
prev
)
=>
{
const
next
=
new
Set
(
prev
);
if
(
next
.
has
(
key
))
{
next
.
delete
(
key
);
}
else
{
next
.
add
(
key
);
}
return
next
;
});
},
[]);
const
setOpen
=
useCallback
((
key
:
string
,
open
:
boolean
)
=>
{
if
(
open
)
{
setOpenDialogs
((
prev
)
=>
new
Set
([...
prev
,
key
]));
}
else
{
setOpenDialogs
((
prev
)
=>
{
const
next
=
new
Set
(
prev
);
next
.
delete
(
key
);
return
next
;
});
}
},
[]);
const
closeAll
=
useCallback
(()
=>
{
setOpenDialogs
(
new
Set
());
},
[]);
return
{
isOpen
,
open
,
close
,
toggle
,
setOpen
,
closeAll
,
openDialogs
:
Array
.
from
(
openDialogs
),
};
}
export
default
useDialog
;
web/src/pages/Attachments.tsx
View file @
240d89fb
...
...
@@ -70,7 +70,7 @@ const Attachments = observer(() => {
<
section
className=
"@container w-full max-w-5xl min-h-full flex flex-col justify-start items-center sm:pt-3 md:pt-6 pb-8"
>
{
!
md
&&
<
MobileHeader
/>
}
<
div
className=
"w-full px-4 sm:px-6"
>
<
div
className=
"w-full
shadow
flex flex-col justify-start items-start px-4 py-3 rounded-xl bg-background text-foreground"
>
<
div
className=
"w-full
border border-border
flex flex-col justify-start items-start px-4 py-3 rounded-xl bg-background text-foreground"
>
<
div
className=
"relative w-full flex flex-row justify-between items-center"
>
<
p
className=
"py-1 flex flex-row justify-start items-center select-none opacity-80"
>
<
PaperclipIcon
className=
"w-6 h-auto mr-1 opacity-80"
/>
...
...
web/src/pages/Inboxes.tsx
View file @
240d89fb
...
...
@@ -36,7 +36,7 @@ const Inboxes = observer(() => {
<
section
className=
"@container w-full max-w-5xl min-h-full flex flex-col justify-start items-center sm:pt-3 md:pt-6 pb-8"
>
{
!
md
&&
<
MobileHeader
/>
}
<
div
className=
"w-full px-4 sm:px-6"
>
<
div
className=
"w-full
shadow
flex flex-col justify-start items-start px-4 py-3 rounded-xl bg-background text-foreground"
>
<
div
className=
"w-full
border border-border
flex flex-col justify-start items-start px-4 py-3 rounded-xl bg-background text-foreground"
>
<
div
className=
"relative w-full flex flex-row justify-between items-center"
>
<
p
className=
"py-1 flex flex-row justify-start items-center select-none opacity-80"
>
<
BellIcon
className=
"w-6 h-auto mr-1 opacity-80"
/>
...
...
web/src/pages/Setting.tsx
View file @
240d89fb
...
...
@@ -87,7 +87,7 @@ const Setting = observer(() => {
<
section
className=
"@container w-full max-w-5xl min-h-full flex flex-col justify-start items-start sm:pt-3 md:pt-6 pb-8"
>
{
!
md
&&
<
MobileHeader
/>
}
<
div
className=
"w-full px-4 sm:px-6"
>
<
div
className=
"w-full
shadow
border border-border flex flex-row justify-start items-start px-4 py-3 rounded-xl bg-background text-muted-foreground"
>
<
div
className=
"w-full border border-border flex flex-row justify-start items-start px-4 py-3 rounded-xl bg-background text-muted-foreground"
>
<
div
className=
"hidden sm:flex flex-col justify-start items-start w-40 h-auto shrink-0 py-2"
>
<
span
className=
"text-sm mt-0.5 pl-3 font-mono select-none text-muted-foreground"
>
{
t
(
"common.basic"
)
}
</
span
>
<
div
className=
"w-full flex flex-col justify-start items-start mt-1"
>
...
...
web/src/types/proto/api/v1/attachment_service.ts
View file @
240d89fb
...
...
@@ -852,7 +852,34 @@ export const AttachmentServiceDefinition = {
responseStream
:
false
,
options
:
{
_unknownFields
:
{
8410
:
[
new
Uint8Array
([
13
,
110
,
97
,
109
,
101
,
44
,
102
,
105
,
108
,
101
,
110
,
97
,
109
,
101
])],
8410
:
[
new
Uint8Array
([
23
,
110
,
97
,
109
,
101
,
44
,
102
,
105
,
108
,
101
,
110
,
97
,
109
,
101
,
44
,
116
,
104
,
117
,
109
,
98
,
110
,
97
,
105
,
108
,
]),
],
578365826
:
[
new
Uint8Array
([
39
,
...
...
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