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
c54fcf7a
Unverified
Commit
c54fcf7a
authored
Nov 08, 2025
by
Johnny
Committed by
GitHub
Nov 08, 2025
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
refactor(web): redesign Settings components (#5237)
Co-authored-by:
Claude
<
noreply@anthropic.com
>
parent
805bb4e7
Changes
14
Show whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
829 additions
and
680 deletions
+829
-680
AccessTokenSection.tsx
web/src/components/Settings/AccessTokenSection.tsx
+57
-71
InstanceSection.tsx
web/src/components/Settings/InstanceSection.tsx
+97
-95
MemberSection.tsx
web/src/components/Settings/MemberSection.tsx
+70
-73
MemoRelatedSettings.tsx
web/src/components/Settings/MemoRelatedSettings.tsx
+99
-93
MyAccountSection.tsx
web/src/components/Settings/MyAccountSection.tsx
+38
-30
PreferencesSection.tsx
web/src/components/Settings/PreferencesSection.tsx
+37
-38
SSOSection.tsx
web/src/components/Settings/SSOSection.tsx
+54
-41
SettingGroup.tsx
web/src/components/Settings/SettingGroup.tsx
+34
-0
SettingRow.tsx
web/src/components/Settings/SettingRow.tsx
+45
-0
SettingSection.tsx
web/src/components/Settings/SettingSection.tsx
+35
-0
SettingTable.tsx
web/src/components/Settings/SettingTable.tsx
+69
-0
StorageSection.tsx
web/src/components/Settings/StorageSection.tsx
+68
-85
UserSessionsSection.tsx
web/src/components/Settings/UserSessionsSection.tsx
+81
-97
WebhookSection.tsx
web/src/components/Settings/WebhookSection.tsx
+45
-57
No files found.
web/src/components/Settings/AccessTokenSection.tsx
View file @
c54fcf7a
import
copy
from
"copy-to-clipboard"
;
import
copy
from
"copy-to-clipboard"
;
import
{
ClipboardIcon
,
TrashIcon
}
from
"lucide-react"
;
import
{
ClipboardIcon
,
PlusIcon
,
TrashIcon
}
from
"lucide-react"
;
import
{
useEffect
,
useState
}
from
"react"
;
import
{
useEffect
,
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
{
toast
}
from
"react-hot-toast"
;
import
ConfirmDialog
from
"@/components/ConfirmDialog"
;
import
ConfirmDialog
from
"@/components/ConfirmDialog"
;
...
@@ -10,6 +10,7 @@ import { useDialog } from "@/hooks/useDialog";
...
@@ -10,6 +10,7 @@ import { useDialog } from "@/hooks/useDialog";
import
{
UserAccessToken
}
from
"@/types/proto/api/v1/user_service"
;
import
{
UserAccessToken
}
from
"@/types/proto/api/v1/user_service"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
CreateAccessTokenDialog
from
"../CreateAccessTokenDialog"
;
import
CreateAccessTokenDialog
from
"../CreateAccessTokenDialog"
;
import
SettingTable
from
"./SettingTable"
;
const
listAccessTokens
=
async
(
parent
:
string
)
=>
{
const
listAccessTokens
=
async
(
parent
:
string
)
=>
{
const
{
accessTokens
}
=
await
userServiceClient
.
listUserAccessTokens
({
parent
});
const
{
accessTokens
}
=
await
userServiceClient
.
listUserAccessTokens
({
parent
});
...
@@ -63,78 +64,63 @@ const AccessTokenSection = () => {
...
@@ -63,78 +64,63 @@ const AccessTokenSection = () => {
};
};
return
(
return
(
<
div
className=
"mt-6 w-full flex flex-col justify-start items-start space-y-4"
>
<
div
className=
"w-full flex flex-col gap-2"
>
<
div
className=
"w-full"
>
<
div
className=
"flex flex-col sm:flex-row sm:items-start sm:justify-between gap-2"
>
<
div
className=
"sm:flex sm:items-center sm:justify-between"
>
<
div
className=
"flex flex-col gap-1"
>
<
div
className=
"sm:flex-auto space-y-1"
>
<
h4
className=
"text-sm font-medium text-muted-foreground"
>
{
t
(
"setting.access-token-section.title"
)
}
</
h4
>
<
p
className=
"flex flex-row justify-start items-center font-medium text-muted-foreground"
>
<
p
className=
"text-xs text-muted-foreground"
>
{
t
(
"setting.access-token-section.description"
)
}
</
p
>
{
t
(
"setting.access-token-section.title"
)
}
</
p
>
<
p
className=
"text-sm text-muted-foreground"
>
{
t
(
"setting.access-token-section.description"
)
}
</
p
>
</
div
>
</
div
>
<
div
className=
"mt-4 sm:mt-0
"
>
<
Button
onClick=
{
handleCreateToken
}
size=
"sm
"
>
<
Button
color=
"primary"
onClick=
{
handleCreateToken
}
>
<
PlusIcon
className=
"w-4 h-4 mr-1.5"
/
>
{
t
(
"common.create"
)
}
{
t
(
"common.create"
)
}
</
Button
>
</
Button
>
</
div
>
</
div
>
</
div
>
<
div
className=
"w-full mt-2 flow-root"
>
<
SettingTable
<
div
className=
"overflow-x-auto"
>
columns=
{
[
<
div
className=
"inline-block min-w-full border border-border rounded-lg align-middle"
>
{
<
table
className=
"min-w-full divide-y divide-border"
>
key
:
"accessToken"
,
<
thead
>
header
:
t
(
"setting.access-token-section.token"
),
<
tr
>
render
:
(
_
,
token
:
UserAccessToken
)
=>
(
<
th
scope=
"col"
className=
"px-3 py-2 text-left text-sm font-semibold text-foreground"
>
<
div
className=
"flex items-center gap-1"
>
{
t
(
"setting.access-token-section.token"
)
}
<
span
className=
"font-mono text-foreground"
>
{
getFormatedAccessToken
(
token
.
accessToken
)
}
</
span
>
</
th
>
<
Button
variant=
"ghost"
size=
"sm"
onClick=
{
()
=>
copyAccessToken
(
token
.
accessToken
)
}
>
<
th
scope=
"col"
className=
"py-2 pl-4 pr-3 text-left text-sm font-semibold text-foreground"
>
{
t
(
"common.description"
)
}
</
th
>
<
th
scope=
"col"
className=
"px-3 py-2 text-left text-sm font-semibold text-foreground"
>
{
t
(
"setting.access-token-section.create-dialog.created-at"
)
}
</
th
>
<
th
scope=
"col"
className=
"px-3 py-2 text-left text-sm font-semibold text-foreground"
>
{
t
(
"setting.access-token-section.create-dialog.expires-at"
)
}
</
th
>
<
th
scope=
"col"
className=
"relative py-3.5 pl-3 pr-4"
>
<
span
className=
"sr-only"
>
{
t
(
"common.delete"
)
}
</
span
>
</
th
>
</
tr
>
</
thead
>
<
tbody
className=
"divide-y divide-border"
>
{
userAccessTokens
.
map
((
userAccessToken
)
=>
(
<
tr
key=
{
userAccessToken
.
accessToken
}
>
<
td
className=
"whitespace-nowrap px-3 py-2 text-sm text-foreground flex flex-row justify-start items-center gap-x-1"
>
<
span
className=
"font-mono"
>
{
getFormatedAccessToken
(
userAccessToken
.
accessToken
)
}
</
span
>
<
Button
variant=
"ghost"
onClick=
{
()
=>
copyAccessToken
(
userAccessToken
.
accessToken
)
}
>
<
ClipboardIcon
className=
"w-4 h-auto text-muted-foreground"
/>
<
ClipboardIcon
className=
"w-4 h-auto text-muted-foreground"
/>
</
Button
>
</
Button
>
</
td
>
</
div
>
<
td
className=
"whitespace-nowrap py-2 pl-4 pr-3 text-sm text-foreground"
>
{
userAccessToken
.
description
}
</
td
>
),
<
td
className=
"whitespace-nowrap px-3 py-2 text-sm text-muted-foreground"
>
},
{
userAccessToken
.
issuedAt
?.
toLocaleString
()
}
{
</
td
>
key
:
"description"
,
<
td
className=
"whitespace-nowrap px-3 py-2 text-sm text-muted-foreground"
>
header
:
t
(
"common.description"
),
{
userAccessToken
.
expiresAt
?.
toLocaleString
()
??
t
(
"setting.access-token-section.create-dialog.duration-never"
)
}
render
:
(
_
,
token
:
UserAccessToken
)
=>
<
span
className=
"text-foreground"
>
{
token
.
description
}
</
span
>,
</
td
>
},
<
td
className=
"relative whitespace-nowrap py-2 pl-3 pr-4 text-right text-sm"
>
{
<
Button
key
:
"issuedAt"
,
variant=
"ghost"
header
:
t
(
"setting.access-token-section.create-dialog.created-at"
),
onClick=
{
()
=>
{
render
:
(
_
,
token
:
UserAccessToken
)
=>
token
.
issuedAt
?.
toLocaleString
(),
handleDeleteAccessToken
(
userAccessToken
);
},
}
}
{
>
key
:
"expiresAt"
,
header
:
t
(
"setting.access-token-section.create-dialog.expires-at"
),
render
:
(
_
,
token
:
UserAccessToken
)
=>
token
.
expiresAt
?.
toLocaleString
()
??
t
(
"setting.access-token-section.create-dialog.duration-never"
),
},
{
key
:
"actions"
,
header
:
""
,
className
:
"text-right"
,
render
:
(
_
,
token
:
UserAccessToken
)
=>
(
<
Button
variant=
"ghost"
size=
"sm"
onClick=
{
()
=>
handleDeleteAccessToken
(
token
)
}
>
<
TrashIcon
className=
"text-destructive w-4 h-auto"
/>
<
TrashIcon
className=
"text-destructive w-4 h-auto"
/>
</
Button
>
</
Button
>
</
td
>
),
</
tr
>
},
))
}
]
}
</
tbody
>
data=
{
userAccessTokens
}
</
table
>
emptyMessage=
"No access tokens found"
</
div
>
getRowKey=
{
(
token
)
=>
token
.
name
}
</
div
>
/>
</
div
>
</
div
>
{
/* Create Access Token Dialog */
}
{
/* Create Access Token Dialog */
}
<
CreateAccessTokenDialog
<
CreateAccessTokenDialog
...
...
web/src/components/Settings/InstanceSection.tsx
View file @
c54fcf7a
...
@@ -4,7 +4,6 @@ import { useEffect, useState } from "react";
...
@@ -4,7 +4,6 @@ import { useEffect, useState } from "react";
import
{
toast
}
from
"react-hot-toast"
;
import
{
toast
}
from
"react-hot-toast"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Select
,
SelectContent
,
SelectItem
,
SelectTrigger
,
SelectValue
}
from
"@/components/ui/select"
;
import
{
Select
,
SelectContent
,
SelectItem
,
SelectTrigger
,
SelectValue
}
from
"@/components/ui/select"
;
import
{
Separator
}
from
"@/components/ui/separator"
;
import
{
Switch
}
from
"@/components/ui/switch"
;
import
{
Switch
}
from
"@/components/ui/switch"
;
import
{
Textarea
}
from
"@/components/ui/textarea"
;
import
{
Textarea
}
from
"@/components/ui/textarea"
;
import
{
identityProviderServiceClient
}
from
"@/grpcweb"
;
import
{
identityProviderServiceClient
}
from
"@/grpcweb"
;
...
@@ -16,6 +15,9 @@ import { InstanceSetting_GeneralSetting, InstanceSetting_Key } from "@/types/pro
...
@@ -16,6 +15,9 @@ import { InstanceSetting_GeneralSetting, InstanceSetting_Key } from "@/types/pro
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
ThemeSelect
from
"../ThemeSelect"
;
import
ThemeSelect
from
"../ThemeSelect"
;
import
UpdateCustomizedProfileDialog
from
"../UpdateCustomizedProfileDialog"
;
import
UpdateCustomizedProfileDialog
from
"../UpdateCustomizedProfileDialog"
;
import
SettingGroup
from
"./SettingGroup"
;
import
SettingRow
from
"./SettingRow"
;
import
SettingSection
from
"./SettingSection"
;
const
InstanceSection
=
observer
(()
=>
{
const
InstanceSection
=
observer
(()
=>
{
const
t
=
useTranslate
();
const
t
=
useTranslate
();
...
@@ -67,30 +69,25 @@ const InstanceSection = observer(() => {
...
@@ -67,30 +69,25 @@ const InstanceSection = observer(() => {
};
};
return
(
return
(
<
div
className=
"w-full flex flex-col gap-2 pt-2 pb-4"
>
<
SettingSection
>
<
p
className=
"font-medium text-foreground"
>
{
t
(
"common.basic"
)
}
</
p
>
<
SettingGroup
title=
{
t
(
"common.basic"
)
}
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
SettingRow
label=
{
t
(
"setting.system-section.server-name"
)
}
description=
{
instanceGeneralSetting
.
customProfile
?.
title
||
"Memos"
}
>
<
div
>
{
t
(
"setting.system-section.server-name"
)
}
:
{
" "
}
<
span
className=
"font-mono font-bold"
>
{
instanceGeneralSetting
.
customProfile
?.
title
||
"Memos"
}
</
span
>
</
div
>
<
Button
variant=
"outline"
onClick=
{
handleUpdateCustomizedProfileButtonClick
}
>
<
Button
variant=
"outline"
onClick=
{
handleUpdateCustomizedProfileButtonClick
}
>
{
t
(
"common.edit"
)
}
{
t
(
"common.edit"
)
}
</
Button
>
</
Button
>
</
div
>
</
SettingRow
>
<
Separator
/
>
<
/
SettingGroup
>
<
p
className=
"font-medium text-foreground"
>
{
t
(
"setting.system-section.title"
)
}
</
p
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
SettingGroup
title=
{
t
(
"setting.system-section.title"
)
}
showSeparator
>
<
span
>
Theme
</
span
>
<
SettingRow
label=
"Theme"
>
<
ThemeSelect
<
ThemeSelect
value=
{
instanceGeneralSetting
.
theme
||
"default"
}
value=
{
instanceGeneralSetting
.
theme
||
"default"
}
onValueChange=
{
(
value
:
string
)
=>
updatePartialSetting
({
theme
:
value
})
}
onValueChange=
{
(
value
:
string
)
=>
updatePartialSetting
({
theme
:
value
})
}
className=
"min-w-fit"
className=
"min-w-fit"
/>
/>
</
div
>
</
SettingRow
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
span
>
{
t
(
"setting.system-section.additional-style"
)
}
</
span
>
<
SettingRow
label=
{
t
(
"setting.system-section.additional-style"
)
}
vertical
>
</
div
>
<
Textarea
<
Textarea
className=
"font-mono w-full"
className=
"font-mono w-full"
rows=
{
3
}
rows=
{
3
}
...
@@ -98,9 +95,9 @@ const InstanceSection = observer(() => {
...
@@ -98,9 +95,9 @@ const InstanceSection = observer(() => {
value=
{
instanceGeneralSetting
.
additionalStyle
}
value=
{
instanceGeneralSetting
.
additionalStyle
}
onChange=
{
(
event
)
=>
updatePartialSetting
({
additionalStyle
:
event
.
target
.
value
})
}
onChange=
{
(
event
)
=>
updatePartialSetting
({
additionalStyle
:
event
.
target
.
value
})
}
/>
/>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
</
SettingRow
>
<
span
>
{
t
(
"setting.system-section.additional-script"
)
}
</
span
>
</
div
>
<
SettingRow
label=
{
t
(
"setting.system-section.additional-script"
)
}
vertical
>
<
Textarea
<
Textarea
className=
"font-mono w-full"
className=
"font-mono w-full"
rows=
{
3
}
rows=
{
3
}
...
@@ -108,16 +105,19 @@ const InstanceSection = observer(() => {
...
@@ -108,16 +105,19 @@ const InstanceSection = observer(() => {
value=
{
instanceGeneralSetting
.
additionalScript
}
value=
{
instanceGeneralSetting
.
additionalScript
}
onChange=
{
(
event
)
=>
updatePartialSetting
({
additionalScript
:
event
.
target
.
value
})
}
onChange=
{
(
event
)
=>
updatePartialSetting
({
additionalScript
:
event
.
target
.
value
})
}
/>
/>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
</
SettingRow
>
<
span
>
{
t
(
"setting.instance-section.disallow-user-registration"
)
}
</
span
>
</
SettingGroup
>
<
SettingGroup
title=
{
t
(
"setting.instance-section.disallow-user-registration"
)
}
showSeparator
>
<
SettingRow
label=
{
t
(
"setting.instance-section.disallow-user-registration"
)
}
>
<
Switch
<
Switch
disabled=
{
instanceStore
.
state
.
profile
.
mode
===
"demo"
}
disabled=
{
instanceStore
.
state
.
profile
.
mode
===
"demo"
}
checked=
{
instanceGeneralSetting
.
disallowUserRegistration
}
checked=
{
instanceGeneralSetting
.
disallowUserRegistration
}
onCheckedChange=
{
(
checked
)
=>
updatePartialSetting
({
disallowUserRegistration
:
checked
})
}
onCheckedChange=
{
(
checked
)
=>
updatePartialSetting
({
disallowUserRegistration
:
checked
})
}
/>
/>
</
div
>
</
SettingRow
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
span
>
{
t
(
"setting.instance-section.disallow-password-auth"
)
}
</
span
>
<
SettingRow
label=
{
t
(
"setting.instance-section.disallow-password-auth"
)
}
>
<
Switch
<
Switch
disabled=
{
disabled=
{
instanceStore
.
state
.
profile
.
mode
===
"demo"
||
instanceStore
.
state
.
profile
.
mode
===
"demo"
||
...
@@ -126,23 +126,23 @@ const InstanceSection = observer(() => {
...
@@ -126,23 +126,23 @@ const InstanceSection = observer(() => {
checked=
{
instanceGeneralSetting
.
disallowPasswordAuth
}
checked=
{
instanceGeneralSetting
.
disallowPasswordAuth
}
onCheckedChange=
{
(
checked
)
=>
updatePartialSetting
({
disallowPasswordAuth
:
checked
})
}
onCheckedChange=
{
(
checked
)
=>
updatePartialSetting
({
disallowPasswordAuth
:
checked
})
}
/>
/>
</
div
>
</
SettingRow
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
span
>
{
t
(
"setting.instance-section.disallow-change-username"
)
}
</
span
>
<
SettingRow
label=
{
t
(
"setting.instance-section.disallow-change-username"
)
}
>
<
Switch
<
Switch
checked=
{
instanceGeneralSetting
.
disallowChangeUsername
}
checked=
{
instanceGeneralSetting
.
disallowChangeUsername
}
onCheckedChange=
{
(
checked
)
=>
updatePartialSetting
({
disallowChangeUsername
:
checked
})
}
onCheckedChange=
{
(
checked
)
=>
updatePartialSetting
({
disallowChangeUsername
:
checked
})
}
/>
/>
</
div
>
</
SettingRow
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
span
>
{
t
(
"setting.instance-section.disallow-change-nickname"
)
}
</
span
>
<
SettingRow
label=
{
t
(
"setting.instance-section.disallow-change-nickname"
)
}
>
<
Switch
<
Switch
checked=
{
instanceGeneralSetting
.
disallowChangeNickname
}
checked=
{
instanceGeneralSetting
.
disallowChangeNickname
}
onCheckedChange=
{
(
checked
)
=>
updatePartialSetting
({
disallowChangeNickname
:
checked
})
}
onCheckedChange=
{
(
checked
)
=>
updatePartialSetting
({
disallowChangeNickname
:
checked
})
}
/>
/>
</
div
>
</
SettingRow
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
span
className=
"truncate"
>
{
t
(
"setting.instance-section.week-start-day"
)
}
</
span
>
<
SettingRow
label=
{
t
(
"setting.instance-section.week-start-day"
)
}
>
<
Select
<
Select
value=
{
instanceGeneralSetting
.
weekStartDayOffset
.
toString
()
}
value=
{
instanceGeneralSetting
.
weekStartDayOffset
.
toString
()
}
onValueChange=
{
(
value
)
=>
{
onValueChange=
{
(
value
)
=>
{
...
@@ -158,8 +158,10 @@ const InstanceSection = observer(() => {
...
@@ -158,8 +158,10 @@ const InstanceSection = observer(() => {
<
SelectItem
value=
"1"
>
{
t
(
"setting.instance-section.monday"
)
}
</
SelectItem
>
<
SelectItem
value=
"1"
>
{
t
(
"setting.instance-section.monday"
)
}
</
SelectItem
>
</
SelectContent
>
</
SelectContent
>
</
Select
>
</
Select
>
</
div
>
</
SettingRow
>
<
div
className=
"mt-2 w-full flex justify-end"
>
</
SettingGroup
>
<
div
className=
"w-full flex justify-end"
>
<
Button
disabled=
{
isEqual
(
instanceGeneralSetting
,
originalSetting
)
}
onClick=
{
handleSaveGeneralSetting
}
>
<
Button
disabled=
{
isEqual
(
instanceGeneralSetting
,
originalSetting
)
}
onClick=
{
handleSaveGeneralSetting
}
>
{
t
(
"common.save"
)
}
{
t
(
"common.save"
)
}
</
Button
>
</
Button
>
...
@@ -173,7 +175,7 @@ const InstanceSection = observer(() => {
...
@@ -173,7 +175,7 @@ const InstanceSection = observer(() => {
toast
.
success
(
"Profile updated successfully!"
);
toast
.
success
(
"Profile updated successfully!"
);
}
}
}
}
/>
/>
</
div
>
</
SettingSection
>
);
);
});
});
...
...
web/src/components/Settings/MemberSection.tsx
View file @
c54fcf7a
...
@@ -14,6 +14,8 @@ import { User, User_Role } from "@/types/proto/api/v1/user_service";
...
@@ -14,6 +14,8 @@ import { User, User_Role } from "@/types/proto/api/v1/user_service";
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
CreateUserDialog
from
"../CreateUserDialog"
;
import
CreateUserDialog
from
"../CreateUserDialog"
;
import
{
DropdownMenu
,
DropdownMenuContent
,
DropdownMenuItem
,
DropdownMenuTrigger
}
from
"../ui/dropdown-menu"
;
import
{
DropdownMenu
,
DropdownMenuContent
,
DropdownMenuItem
,
DropdownMenuTrigger
}
from
"../ui/dropdown-menu"
;
import
SettingSection
from
"./SettingSection"
;
import
SettingTable
from
"./SettingTable"
;
const
MemberSection
=
observer
(()
=>
{
const
MemberSection
=
observer
(()
=>
{
const
t
=
useTranslate
();
const
t
=
useTranslate
();
...
@@ -101,54 +103,53 @@ const MemberSection = observer(() => {
...
@@ -101,54 +103,53 @@ const MemberSection = observer(() => {
};
};
return
(
return
(
<
div
className=
"w-full flex flex-col gap-2 pt-2 pb-4"
>
<
SettingSection
<
div
className=
"w-full flex flex-row justify-between items-center"
>
title=
{
t
(
"setting.member-list"
)
}
<
p
className=
"font-medium text-muted-foreground"
>
{
t
(
"setting.member-section.create-a-member"
)
}
</
p
>
actions=
{
<
Button
onClick=
{
handleCreateUser
}
>
<
Button
onClick=
{
handleCreateUser
}
>
<
PlusIcon
className=
"w-4 h-4 mr-2"
/>
<
PlusIcon
className=
"w-4 h-4 mr-2"
/>
{
t
(
"common.create"
)
}
{
t
(
"common.create"
)
}
</
Button
>
</
Button
>
</
div
>
}
<
div
className=
"w-full flex flex-row justify-between items-center mt-6"
>
>
<
div
className=
"title-text"
>
{
t
(
"setting.member-list"
)
}
</
div
>
<
SettingTable
</
div
>
columns=
{
[
<
div
className=
"w-full overflow-x-auto"
>
{
<
div
className=
"inline-block min-w-full align-middle border border-border rounded-lg"
>
key
:
"username"
,
<
table
className=
"min-w-full divide-y divide-border"
>
header
:
t
(
"common.username"
),
<
thead
>
render
:
(
_
,
user
:
User
)
=>
(
<
tr
className=
"text-sm font-semibold text-left text-foreground"
>
<
span
className=
"text-foreground"
>
<
th
scope=
"col"
className=
"px-3 py-2"
>
{
t
(
"common.username"
)
}
</
th
>
<
th
scope=
"col"
className=
"px-3 py-2"
>
{
t
(
"common.role"
)
}
</
th
>
<
th
scope=
"col"
className=
"px-3 py-2"
>
{
t
(
"common.nickname"
)
}
</
th
>
<
th
scope=
"col"
className=
"px-3 py-2"
>
{
t
(
"common.email"
)
}
</
th
>
<
th
scope=
"col"
className=
"relative py-2 pl-3 pr-4"
></
th
>
</
tr
>
</
thead
>
<
tbody
className=
"divide-y divide-border"
>
{
sortedUsers
.
map
((
user
)
=>
(
<
tr
key=
{
user
.
name
}
>
<
td
className=
"whitespace-nowrap px-3 py-2 text-sm text-muted-foreground"
>
{
user
.
username
}
{
user
.
username
}
<
span
className=
"ml-1 italic"
>
{
user
.
state
===
State
.
ARCHIVED
&&
"(Archived)"
}
</
span
>
{
user
.
state
===
State
.
ARCHIVED
&&
<
span
className=
"ml-2 italic text-muted-foreground"
>
(Archived)
</
span
>
}
</
td
>
</
span
>
<
td
className=
"whitespace-nowrap px-3 py-2 text-sm text-muted-foreground"
>
{
stringifyUserRole
(
user
.
role
)
}
</
td
>
),
<
td
className=
"whitespace-nowrap px-3 py-2 text-sm text-muted-foreground"
>
{
user
.
displayName
}
</
td
>
},
<
td
className=
"whitespace-nowrap px-3 py-2 text-sm text-muted-foreground"
>
{
user
.
email
}
</
td
>
{
<
td
className=
"relative whitespace-nowrap py-2 pl-3 pr-4 text-right text-sm font-medium flex justify-end"
>
key
:
"role"
,
{
currentUser
?.
name
===
user
.
name
?
(
header
:
t
(
"common.role"
),
<
span
>
{
t
(
"common.yourself"
)
}
</
span
>
render
:
(
_
,
user
:
User
)
=>
stringifyUserRole
(
user
.
role
),
},
{
key
:
"displayName"
,
header
:
t
(
"common.nickname"
),
render
:
(
_
,
user
:
User
)
=>
user
.
displayName
,
},
{
key
:
"email"
,
header
:
t
(
"common.email"
),
render
:
(
_
,
user
:
User
)
=>
user
.
email
,
},
{
key
:
"actions"
,
header
:
""
,
className
:
"text-right"
,
render
:
(
_
,
user
:
User
)
=>
currentUser
?.
name
===
user
.
name
?
(
<
span
className=
"text-muted-foreground"
>
{
t
(
"common.yourself"
)
}
</
span
>
)
:
(
)
:
(
<
DropdownMenu
>
<
DropdownMenu
>
<
DropdownMenuTrigger
asChild
>
<
DropdownMenuTrigger
asChild
>
<
Button
variant=
"outline
"
>
<
Button
variant=
"outline"
size=
"sm
"
>
<
MoreVerticalIcon
className=
"w-4 h-auto"
/>
<
MoreVerticalIcon
className=
"w-4 h-auto"
/>
</
Button
>
</
Button
>
</
DropdownMenuTrigger
>
</
DropdownMenuTrigger
>
...
@@ -161,24 +162,20 @@ const MemberSection = observer(() => {
...
@@ -161,24 +162,20 @@ const MemberSection = observer(() => {
)
:
(
)
:
(
<>
<>
<
DropdownMenuItem
onClick=
{
()
=>
handleRestoreUserClick
(
user
)
}
>
{
t
(
"common.restore"
)
}
</
DropdownMenuItem
>
<
DropdownMenuItem
onClick=
{
()
=>
handleRestoreUserClick
(
user
)
}
>
{
t
(
"common.restore"
)
}
</
DropdownMenuItem
>
<
DropdownMenuItem
<
DropdownMenuItem
onClick=
{
()
=>
handleDeleteUserClick
(
user
)
}
className=
"text-destructive focus:text-destructive"
>
onClick=
{
()
=>
handleDeleteUserClick
(
user
)
}
className=
"text-destructive focus:text-destructive"
>
{
t
(
"setting.member-section.delete-member"
)
}
{
t
(
"setting.member-section.delete-member"
)
}
</
DropdownMenuItem
>
</
DropdownMenuItem
>
</>
</>
)
}
)
}
</
DropdownMenuContent
>
</
DropdownMenuContent
>
</
DropdownMenu
>
</
DropdownMenu
>
)
}
),
</
td
>
},
</
tr
>
]
}
))
}
data=
{
sortedUsers
}
</
tbody
>
emptyMessage=
"No members found"
</
table
>
getRowKey=
{
(
user
)
=>
user
.
name
}
</
div
>
/>
</
div
>
{
/* Create User Dialog */
}
{
/* Create User Dialog */
}
<
CreateUserDialog
open=
{
createDialog
.
isOpen
}
onOpenChange=
{
createDialog
.
setOpen
}
onSuccess=
{
fetchUsers
}
/>
<
CreateUserDialog
open=
{
createDialog
.
isOpen
}
onOpenChange=
{
createDialog
.
setOpen
}
onSuccess=
{
fetchUsers
}
/>
...
@@ -207,7 +204,7 @@ const MemberSection = observer(() => {
...
@@ -207,7 +204,7 @@ const MemberSection = observer(() => {
onConfirm=
{
confirmDeleteUser
}
onConfirm=
{
confirmDeleteUser
}
confirmVariant=
"destructive"
confirmVariant=
"destructive"
/>
/>
</
div
>
</
SettingSection
>
);
);
});
});
...
...
web/src/components/Settings/MemoRelatedSettings.tsx
View file @
c54fcf7a
...
@@ -11,6 +11,9 @@ import { instanceStore } from "@/store";
...
@@ -11,6 +11,9 @@ import { instanceStore } from "@/store";
import
{
instanceSettingNamePrefix
}
from
"@/store/common"
;
import
{
instanceSettingNamePrefix
}
from
"@/store/common"
;
import
{
InstanceSetting_MemoRelatedSetting
,
InstanceSetting_Key
}
from
"@/types/proto/api/v1/instance_service"
;
import
{
InstanceSetting_MemoRelatedSetting
,
InstanceSetting_Key
}
from
"@/types/proto/api/v1/instance_service"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
SettingGroup
from
"./SettingGroup"
;
import
SettingRow
from
"./SettingRow"
;
import
SettingSection
from
"./SettingSection"
;
const
MemoRelatedSettings
=
observer
(()
=>
{
const
MemoRelatedSettings
=
observer
(()
=>
{
const
t
=
useTranslate
();
const
t
=
useTranslate
();
...
@@ -65,122 +68,125 @@ const MemoRelatedSettings = observer(() => {
...
@@ -65,122 +68,125 @@ const MemoRelatedSettings = observer(() => {
};
};
return
(
return
(
<
div
className=
"w-full flex flex-col gap-2 pt-2 pb-4"
>
<
SettingSection
>
<
p
className=
"font-medium text-muted-foreground"
>
{
t
(
"setting.memo-related-settings.title"
)
}
</
p
>
<
SettingGroup
title=
{
t
(
"setting.memo-related-settings.title"
)
}
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
SettingRow
label=
{
t
(
"setting.system-section.disable-public-memos"
)
}
>
<
span
>
{
t
(
"setting.system-section.disable-public-memos"
)
}
</
span
>
<
Switch
<
Switch
checked=
{
memoRelatedSetting
.
disallowPublicVisibility
}
checked=
{
memoRelatedSetting
.
disallowPublicVisibility
}
onCheckedChange=
{
(
checked
)
=>
updatePartialSetting
({
disallowPublicVisibility
:
checked
})
}
onCheckedChange=
{
(
checked
)
=>
updatePartialSetting
({
disallowPublicVisibility
:
checked
})
}
/>
/>
</
div
>
</
SettingRow
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
span
>
{
t
(
"setting.system-section.display-with-updated-time"
)
}
</
span
>
<
SettingRow
label=
{
t
(
"setting.system-section.display-with-updated-time"
)
}
>
<
Switch
<
Switch
checked=
{
memoRelatedSetting
.
displayWithUpdateTime
}
checked=
{
memoRelatedSetting
.
displayWithUpdateTime
}
onCheckedChange=
{
(
checked
)
=>
updatePartialSetting
({
displayWithUpdateTime
:
checked
})
}
onCheckedChange=
{
(
checked
)
=>
updatePartialSetting
({
displayWithUpdateTime
:
checked
})
}
/>
/>
</
div
>
</
SettingRow
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
span
>
{
t
(
"setting.memo-related-settings.enable-link-preview"
)
}
</
span
>
<
SettingRow
label=
{
t
(
"setting.memo-related-settings.enable-link-preview"
)
}
>
<
Switch
<
Switch
checked=
{
memoRelatedSetting
.
enableLinkPreview
}
checked=
{
memoRelatedSetting
.
enableLinkPreview
}
onCheckedChange=
{
(
checked
)
=>
updatePartialSetting
({
enableLinkPreview
:
checked
})
}
onCheckedChange=
{
(
checked
)
=>
updatePartialSetting
({
enableLinkPreview
:
checked
})
}
/>
/>
</
div
>
</
SettingRow
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
span
>
{
t
(
"setting.system-section.enable-double-click-to-edit"
)
}
</
span
>
<
SettingRow
label=
{
t
(
"setting.system-section.enable-double-click-to-edit"
)
}
>
<
Switch
<
Switch
checked=
{
memoRelatedSetting
.
enableDoubleClickEdit
}
checked=
{
memoRelatedSetting
.
enableDoubleClickEdit
}
onCheckedChange=
{
(
checked
)
=>
updatePartialSetting
({
enableDoubleClickEdit
:
checked
})
}
onCheckedChange=
{
(
checked
)
=>
updatePartialSetting
({
enableDoubleClickEdit
:
checked
})
}
/>
/>
</
div
>
</
SettingRow
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
span
>
{
t
(
"setting.system-section.disable-markdown-shortcuts-in-editor"
)
}
</
span
>
<
SettingRow
label=
{
t
(
"setting.system-section.disable-markdown-shortcuts-in-editor"
)
}
>
<
Switch
<
Switch
checked=
{
memoRelatedSetting
.
disableMarkdownShortcuts
}
checked=
{
memoRelatedSetting
.
disableMarkdownShortcuts
}
onCheckedChange=
{
(
checked
)
=>
updatePartialSetting
({
disableMarkdownShortcuts
:
checked
})
}
onCheckedChange=
{
(
checked
)
=>
updatePartialSetting
({
disableMarkdownShortcuts
:
checked
})
}
/>
/>
</
div
>
</
SettingRow
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
span
>
{
t
(
"setting.memo-related-settings.content-lenght-limit"
)
}
</
span
>
<
SettingRow
label=
{
t
(
"setting.memo-related-settings.content-lenght-limit"
)
}
>
<
Input
<
Input
className=
"w-24"
className=
"w-24"
type=
"number"
type=
"number"
defaultValue=
{
memoRelatedSetting
.
contentLengthLimit
}
defaultValue=
{
memoRelatedSetting
.
contentLengthLimit
}
onBlur=
{
(
event
)
=>
updatePartialSetting
({
contentLengthLimit
:
Number
(
event
.
target
.
value
)
})
}
onBlur=
{
(
event
)
=>
updatePartialSetting
({
contentLengthLimit
:
Number
(
event
.
target
.
value
)
})
}
/>
/>
</
div
>
</
SettingRow
>
<
div
className=
"w-full"
>
<
/
SettingGroup
>
<
span
className=
"truncate"
>
{
t
(
"setting.memo-related-settings.reactions"
)
}
</
span
>
<
div
className=
"mt-2 w-full flex flex-row flex-wrap gap-1"
>
<
SettingGroup
title=
{
t
(
"setting.memo-related-settings.reactions"
)
}
showSeparator
>
{
memoRelatedSetting
.
reactions
.
map
((
reactionType
)
=>
{
<
div
className=
"w-full flex flex-row flex-wrap gap-2"
>
return
(
{
memoRelatedSetting
.
reactions
.
map
((
reactionType
)
=>
(
<
Badge
key=
{
reactionType
}
variant=
"outline"
className=
"flex items-center gap-1 h-8
"
>
<
Badge
key=
{
reactionType
}
variant=
"outline"
className=
"flex items-center gap-1.5 h-8 px-3
"
>
{
reactionType
}
{
reactionType
}
<
span
<
span
className=
"cursor-pointer text-muted-foreground hover:text-primary
"
className=
"cursor-pointer text-muted-foreground hover:text-destructive
"
onClick=
{
()
=>
updatePartialSetting
({
reactions
:
memoRelatedSetting
.
reactions
.
filter
((
r
)
=>
r
!==
reactionType
)
})
}
onClick=
{
()
=>
updatePartialSetting
({
reactions
:
memoRelatedSetting
.
reactions
.
filter
((
r
)
=>
r
!==
reactionType
)
})
}
>
>
<
X
className=
"w-4 h-4
"
/>
<
X
className=
"w-3.5 h-3.5
"
/>
</
span
>
</
span
>
</
Badge
>
</
Badge
>
);
))
}
})
}
<
div
className=
"flex items-center gap-1.5"
>
<
div
className=
"flex items-center gap-1"
>
<
Input
<
Input
className=
"w-32"
className=
"w-32
h-8
"
placeholder=
{
t
(
"common.input"
)
}
placeholder=
{
t
(
"common.input"
)
}
value=
{
editingReaction
}
value=
{
editingReaction
}
onChange=
{
(
event
)
=>
setEditingReaction
(
event
.
target
.
value
.
trim
())
}
onChange=
{
(
event
)
=>
setEditingReaction
(
event
.
target
.
value
.
trim
())
}
onKeyDown=
{
(
e
)
=>
e
.
key
===
"Enter"
&&
upsertReaction
()
}
/>
/>
<
span
className=
"text-muted-foreground cursor-pointer hover:text-primary"
onClick=
{
()
=>
upsertReaction
()
}
>
<
Button
variant=
"ghost"
size=
"sm"
onClick=
{
upsertReaction
}
className=
"h-8 w-8 p-0"
>
<
CheckIcon
className=
"w-5 h-5"
/>
<
CheckIcon
className=
"w-4 h-4"
/>
</
span
>
</
Button
>
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
<
div
className=
"w-full"
>
</
SettingGroup
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
span
>
{
t
(
"setting.memo-related-settings.enable-blur-nsfw-content"
)
}
</
span
>
<
SettingGroup
showSeparator
>
<
SettingRow
label=
{
t
(
"setting.memo-related-settings.enable-blur-nsfw-content"
)
}
>
<
Switch
<
Switch
checked=
{
memoRelatedSetting
.
enableBlurNsfwContent
}
checked=
{
memoRelatedSetting
.
enableBlurNsfwContent
}
onCheckedChange=
{
(
checked
)
=>
updatePartialSetting
({
enableBlurNsfwContent
:
checked
})
}
onCheckedChange=
{
(
checked
)
=>
updatePartialSetting
({
enableBlurNsfwContent
:
checked
})
}
/>
/>
</
div
>
</
SettingRow
>
<
div
className=
"mt-2 w-full flex flex-row flex-wrap gap-1"
>
{
memoRelatedSetting
.
nsfwTags
.
map
((
nsfwTag
)
=>
{
<
div
className=
"w-full flex flex-col gap-2"
>
return
(
<
span
className=
"text-sm text-muted-foreground"
>
NSFW Tags
</
span
>
<
Badge
key=
{
nsfwTag
}
variant=
"outline"
className=
"flex items-center gap-1 h-8"
>
<
div
className=
"w-full flex flex-row flex-wrap gap-2"
>
{
memoRelatedSetting
.
nsfwTags
.
map
((
nsfwTag
)
=>
(
<
Badge
key=
{
nsfwTag
}
variant=
"outline"
className=
"flex items-center gap-1.5 h-8 px-3"
>
{
nsfwTag
}
{
nsfwTag
}
<
span
<
span
className=
"cursor-pointer text-muted-foreground hover:text-
primary
"
className=
"cursor-pointer text-muted-foreground hover:text-
destructive
"
onClick=
{
()
=>
updatePartialSetting
({
nsfwTags
:
memoRelatedSetting
.
nsfwTags
.
filter
((
r
)
=>
r
!==
nsfwTag
)
})
}
onClick=
{
()
=>
updatePartialSetting
({
nsfwTags
:
memoRelatedSetting
.
nsfwTags
.
filter
((
r
)
=>
r
!==
nsfwTag
)
})
}
>
>
<
X
className=
"w-
4 h-4
"
/>
<
X
className=
"w-
3.5 h-3.5
"
/>
</
span
>
</
span
>
</
Badge
>
</
Badge
>
);
))
}
})
}
<
div
className=
"flex items-center gap-1.5"
>
<
div
className=
"flex items-center gap-1"
>
<
Input
<
Input
className=
"w-32
"
className=
"w-32 h-8
"
placeholder=
{
t
(
"common.input"
)
}
placeholder=
{
t
(
"common.input"
)
}
value=
{
editingNsfwTag
}
value=
{
editingNsfwTag
}
onChange=
{
(
event
)
=>
setEditingNsfwTag
(
event
.
target
.
value
.
trim
())
}
onChange=
{
(
event
)
=>
setEditingNsfwTag
(
event
.
target
.
value
.
trim
())
}
onKeyDown=
{
(
e
)
=>
e
.
key
===
"Enter"
&&
upsertNsfwTags
()
}
/>
/>
<
span
className=
"text-muted-foreground cursor-pointer hover:text-primary"
onClick=
{
()
=>
upsertNsfwTags
()
}
>
<
Button
variant=
"ghost"
size=
"sm"
onClick=
{
upsertNsfwTags
}
className=
"h-8 w-8 p-0"
>
<
CheckIcon
className=
"w-5 h-5
"
/>
<
CheckIcon
className=
"w-4 h-4
"
/>
</
spa
n
>
</
Butto
n
>
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
<
div
className=
"mt-2 w-full flex justify-end"
>
</
SettingGroup
>
<
div
className=
"w-full flex justify-end"
>
<
Button
disabled=
{
isEqual
(
memoRelatedSetting
,
originalSetting
)
}
onClick=
{
updateSetting
}
>
<
Button
disabled=
{
isEqual
(
memoRelatedSetting
,
originalSetting
)
}
onClick=
{
updateSetting
}
>
{
t
(
"common.save"
)
}
{
t
(
"common.save"
)
}
</
Button
>
</
Button
>
</
div
>
</
div
>
</
div
>
</
SettingSection
>
);
);
});
});
...
...
web/src/components/Settings/MyAccountSection.tsx
View file @
c54fcf7a
...
@@ -8,6 +8,8 @@ import UpdateAccountDialog from "../UpdateAccountDialog";
...
@@ -8,6 +8,8 @@ import UpdateAccountDialog from "../UpdateAccountDialog";
import
UserAvatar
from
"../UserAvatar"
;
import
UserAvatar
from
"../UserAvatar"
;
import
{
DropdownMenu
,
DropdownMenuContent
,
DropdownMenuItem
,
DropdownMenuTrigger
}
from
"../ui/dropdown-menu"
;
import
{
DropdownMenu
,
DropdownMenuContent
,
DropdownMenuItem
,
DropdownMenuTrigger
}
from
"../ui/dropdown-menu"
;
import
AccessTokenSection
from
"./AccessTokenSection"
;
import
AccessTokenSection
from
"./AccessTokenSection"
;
import
SettingGroup
from
"./SettingGroup"
;
import
SettingSection
from
"./SettingSection"
;
import
UserSessionsSection
from
"./UserSessionsSection"
;
import
UserSessionsSection
from
"./UserSessionsSection"
;
const
MyAccountSection
=
()
=>
{
const
MyAccountSection
=
()
=>
{
...
@@ -25,44 +27,50 @@ const MyAccountSection = () => {
...
@@ -25,44 +27,50 @@ const MyAccountSection = () => {
};
};
return
(
return
(
<
div
className=
"w-full gap-2 pt-2 pb-4"
>
<
SettingSection
>
<
p
className=
"font-medium text-muted-foreground"
>
{
t
(
"setting.account-section.title"
)
}
</
p
>
<
SettingGroup
title=
{
t
(
"setting.account-section.title"
)
}
>
<
div
className=
"w-full mt-2 flex flex-row justify-start items-center"
>
<
div
className=
"w-full flex flex-row justify-start items-center gap-3"
>
<
UserAvatar
className=
"mr-2 shrink-0 w-10 h-10"
avatarUrl=
{
user
.
avatarUrl
}
/>
<
UserAvatar
className=
"shrink-0 w-12 h-12"
avatarUrl=
{
user
.
avatarUrl
}
/>
<
div
className=
"max-w-[calc(100%-3rem)] flex flex-col justify-center items-start"
>
<
div
className=
"flex-1 min-w-0 flex flex-col justify-center items-start gap-1"
>
<
p
className=
"w-full"
>
<
div
className=
"w-full"
>
<
span
className=
"text-xl leading-tight font-medium"
>
{
user
.
displayName
}
</
span
>
<
span
className=
"text-lg font-semibold"
>
{
user
.
displayName
}
</
span
>
<
span
className=
"ml-1 text-base leading-tight text-muted-foreground"
>
(
{
user
.
username
}
)
</
span
>
<
span
className=
"ml-2 text-sm text-muted-foreground"
>
@
{
user
.
username
}
</
span
>
</
p
>
<
p
className=
"w-4/5 leading-tight text-sm truncate"
>
{
user
.
description
}
</
p
>
</
div
>
</
div
>
{
user
.
description
&&
<
p
className=
"w-full text-sm text-muted-foreground truncate"
>
{
user
.
description
}
</
p
>
}
</
div
>
</
div
>
<
div
className=
"w-full flex flex-row justify-start items-center mt-2 space-x-2
"
>
<
div
className=
"flex items-center gap-2 shrink-0
"
>
<
Button
variant=
"outline
"
onClick=
{
handleEditAccount
}
>
<
Button
variant=
"outline"
size=
"sm
"
onClick=
{
handleEditAccount
}
>
<
PenLineIcon
className=
"w-4 h-4 mx-auto mr-1
"
/>
<
PenLineIcon
className=
"w-4 h-4 mr-1.5
"
/>
{
t
(
"common.edit"
)
}
{
t
(
"common.edit"
)
}
</
Button
>
</
Button
>
<
DropdownMenu
>
<
DropdownMenu
>
<
DropdownMenuTrigger
asChild
>
<
DropdownMenuTrigger
asChild
>
<
Button
variant=
"outline
"
>
<
Button
variant=
"outline"
size=
"sm
"
>
<
MoreVerticalIcon
className=
"w-4 h-4 mx-auto
"
/>
<
MoreVerticalIcon
className=
"w-4 h-4
"
/>
</
Button
>
</
Button
>
</
DropdownMenuTrigger
>
</
DropdownMenuTrigger
>
<
DropdownMenuContent
align=
"start
"
>
<
DropdownMenuContent
align=
"end
"
>
<
DropdownMenuItem
onClick=
{
handleChangePassword
}
>
{
t
(
"setting.account-section.change-password"
)
}
</
DropdownMenuItem
>
<
DropdownMenuItem
onClick=
{
handleChangePassword
}
>
{
t
(
"setting.account-section.change-password"
)
}
</
DropdownMenuItem
>
</
DropdownMenuContent
>
</
DropdownMenuContent
>
</
DropdownMenu
>
</
DropdownMenu
>
</
div
>
</
div
>
</
div
>
</
SettingGroup
>
<
SettingGroup
showSeparator
>
<
UserSessionsSection
/>
<
UserSessionsSection
/>
</
SettingGroup
>
<
SettingGroup
showSeparator
>
<
AccessTokenSection
/>
<
AccessTokenSection
/>
</
SettingGroup
>
{
/* Update Account Dialog */
}
{
/* Update Account Dialog */
}
<
UpdateAccountDialog
open=
{
accountDialog
.
isOpen
}
onOpenChange=
{
accountDialog
.
setOpen
}
/>
<
UpdateAccountDialog
open=
{
accountDialog
.
isOpen
}
onOpenChange=
{
accountDialog
.
setOpen
}
/>
{
/* Change Password Dialog */
}
{
/* Change Password Dialog */
}
<
ChangeMemberPasswordDialog
open=
{
passwordDialog
.
isOpen
}
onOpenChange=
{
passwordDialog
.
setOpen
}
user=
{
user
}
/>
<
ChangeMemberPasswordDialog
open=
{
passwordDialog
.
isOpen
}
onOpenChange=
{
passwordDialog
.
setOpen
}
user=
{
user
}
/>
</
div
>
</
SettingSection
>
);
);
};
};
...
...
web/src/components/Settings/PreferencesSection.tsx
View file @
c54fcf7a
import
{
observer
}
from
"mobx-react-lite"
;
import
{
observer
}
from
"mobx-react-lite"
;
import
{
Select
,
SelectContent
,
SelectItem
,
SelectTrigger
,
SelectValue
}
from
"@/components/ui/select"
;
import
{
Select
,
SelectContent
,
SelectItem
,
SelectTrigger
,
SelectValue
}
from
"@/components/ui/select"
;
import
{
Separator
}
from
"@/components/ui/separator"
;
import
{
userStore
,
instanceStore
}
from
"@/store"
;
import
{
userStore
,
instanceStore
}
from
"@/store"
;
import
{
Visibility
}
from
"@/types/proto/api/v1/memo_service"
;
import
{
Visibility
}
from
"@/types/proto/api/v1/memo_service"
;
import
{
UserSetting_GeneralSetting
}
from
"@/types/proto/api/v1/user_service"
;
import
{
UserSetting_GeneralSetting
}
from
"@/types/proto/api/v1/user_service"
;
...
@@ -9,6 +8,9 @@ import { convertVisibilityFromString, convertVisibilityToString } from "@/utils/
...
@@ -9,6 +8,9 @@ import { convertVisibilityFromString, convertVisibilityToString } from "@/utils/
import
LocaleSelect
from
"../LocaleSelect"
;
import
LocaleSelect
from
"../LocaleSelect"
;
import
ThemeSelect
from
"../ThemeSelect"
;
import
ThemeSelect
from
"../ThemeSelect"
;
import
VisibilityIcon
from
"../VisibilityIcon"
;
import
VisibilityIcon
from
"../VisibilityIcon"
;
import
SettingGroup
from
"./SettingGroup"
;
import
SettingRow
from
"./SettingRow"
;
import
SettingSection
from
"./SettingSection"
;
import
WebhookSection
from
"./WebhookSection"
;
import
WebhookSection
from
"./WebhookSection"
;
const
PreferencesSection
=
observer
(()
=>
{
const
PreferencesSection
=
observer
(()
=>
{
...
@@ -41,23 +43,19 @@ const PreferencesSection = observer(() => {
...
@@ -41,23 +43,19 @@ const PreferencesSection = observer(() => {
};
};
return
(
return
(
<
div
className=
"w-full flex flex-col gap-2 pt-2 pb-4"
>
<
SettingSection
>
<
p
className=
"font-medium text-muted-foreground"
>
{
t
(
"common.basic"
)
}
</
p
>
<
SettingGroup
title=
{
t
(
"common.basic"
)
}
>
<
SettingRow
label=
{
t
(
"common.language"
)
}
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
span
>
{
t
(
"common.language"
)
}
</
span
>
<
LocaleSelect
value=
{
setting
.
locale
}
onChange=
{
handleLocaleSelectChange
}
/>
<
LocaleSelect
value=
{
setting
.
locale
}
onChange=
{
handleLocaleSelectChange
}
/>
</
div
>
</
SettingRow
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
SettingRow
label=
{
t
(
"setting.preference-section.theme"
)
}
>
<
span
>
{
t
(
"setting.preference-section.theme"
)
}
</
span
>
<
ThemeSelect
value=
{
setting
.
theme
}
onValueChange=
{
handleThemeChange
}
/>
<
ThemeSelect
value=
{
setting
.
theme
}
onValueChange=
{
handleThemeChange
}
/>
</
div
>
</
SettingRow
>
</
SettingGroup
>
<
p
className=
"font-medium text-muted-foreground"
>
{
t
(
"setting.preference"
)
}
</
p
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
SettingGroup
title=
{
t
(
"setting.preference"
)
}
showSeparator
>
<
span
className=
"truncate"
>
{
t
(
"setting.preference-section.default-memo-visibility"
)
}
</
span
>
<
SettingRow
label=
{
t
(
"setting.preference-section.default-memo-visibility"
)
}
>
<
Select
value=
{
setting
.
memoVisibility
}
onValueChange=
{
handleDefaultMemoVisibilityChanged
}
>
<
Select
value=
{
setting
.
memoVisibility
}
onValueChange=
{
handleDefaultMemoVisibilityChanged
}
>
<
SelectTrigger
className=
"min-w-fit"
>
<
SelectTrigger
className=
"min-w-fit"
>
<
div
className=
"flex items-center gap-2"
>
<
div
className=
"flex items-center gap-2"
>
...
@@ -75,12 +73,13 @@ const PreferencesSection = observer(() => {
...
@@ -75,12 +73,13 @@ const PreferencesSection = observer(() => {
))
}
))
}
</
SelectContent
>
</
SelectContent
>
</
Select
>
</
Select
>
</
div
>
</
SettingRow
>
</
SettingGroup
>
<
Separator
className=
"my-3"
/>
<
SettingGroup
showSeparator
>
<
WebhookSection
/>
<
WebhookSection
/>
</
div
>
</
SettingGroup
>
</
SettingSection
>
);
);
});
});
...
...
web/src/components/Settings/SSOSection.tsx
View file @
c54fcf7a
import
{
MoreVerticalIcon
}
from
"lucide-react"
;
import
{
MoreVerticalIcon
,
PlusIcon
}
from
"lucide-react"
;
import
{
useEffect
,
useState
}
from
"react"
;
import
{
useEffect
,
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
{
toast
}
from
"react-hot-toast"
;
import
ConfirmDialog
from
"@/components/ConfirmDialog"
;
import
ConfirmDialog
from
"@/components/ConfirmDialog"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
DropdownMenu
,
DropdownMenuContent
,
DropdownMenuItem
,
DropdownMenuTrigger
}
from
"@/components/ui/dropdown-menu"
;
import
{
DropdownMenu
,
DropdownMenuContent
,
DropdownMenuItem
,
DropdownMenuTrigger
}
from
"@/components/ui/dropdown-menu"
;
import
{
Separator
}
from
"@/components/ui/separator"
;
import
{
identityProviderServiceClient
}
from
"@/grpcweb"
;
import
{
identityProviderServiceClient
}
from
"@/grpcweb"
;
import
{
IdentityProvider
}
from
"@/types/proto/api/v1/idp_service"
;
import
{
IdentityProvider
}
from
"@/types/proto/api/v1/idp_service"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
CreateIdentityProviderDialog
from
"../CreateIdentityProviderDialog"
;
import
CreateIdentityProviderDialog
from
"../CreateIdentityProviderDialog"
;
import
LearnMore
from
"../LearnMore"
;
import
LearnMore
from
"../LearnMore"
;
import
SettingSection
from
"./SettingSection"
;
import
SettingTable
from
"./SettingTable"
;
const
SSOSection
=
()
=>
{
const
SSOSection
=
()
=>
{
const
t
=
useTranslate
();
const
t
=
useTranslate
();
...
@@ -68,48 +69,60 @@ const SSOSection = () => {
...
@@ -68,48 +69,60 @@ const SSOSection = () => {
};
};
return
(
return
(
<
div
className=
"w-full flex flex-col gap-2 pt-2 pb-4"
>
<
SettingSection
<
div
className=
"w-full flex flex-row justify-between items-center gap-1"
>
title=
{
<
div
className=
"flex
flex-row items-center gap-1
"
>
<
div
className=
"flex
items-center gap-2
"
>
<
span
className=
"font-mono text-muted-foreground"
>
{
t
(
"setting.sso-section.sso-list"
)
}
</
span
>
<
span
>
{
t
(
"setting.sso-section.sso-list"
)
}
</
span
>
<
LearnMore
url=
"https://www.usememos.com/docs/configuration/authentication"
/>
<
LearnMore
url=
"https://www.usememos.com/docs/configuration/authentication"
/>
</
div
>
</
div
>
<
Button
color=
"primary"
onClick=
{
handleCreateIdentityProvider
}
>
}
actions=
{
<
Button
onClick=
{
handleCreateIdentityProvider
}
>
<
PlusIcon
className=
"w-4 h-4 mr-2"
/>
{
t
(
"common.create"
)
}
{
t
(
"common.create"
)
}
</
Button
>
</
Button
>
</
div
>
}
<
Separator
/>
{
identityProviderList
.
map
((
identityProvider
)
=>
(
<
div
key=
{
identityProvider
.
name
}
className=
"py-2 w-full border-b last:border-b border-border flex flex-row items-center justify-between"
>
>
<
div
className=
"flex flex-row items-center"
>
<
SettingTable
<
p
className=
"ml-2"
>
columns=
{
[
{
identityProvider
.
title
}
{
<
span
className=
"text-sm ml-1 text-muted-foreground"
>
(
{
identityProvider
.
type
}
)
</
span
>
key
:
"title"
,
</
p
>
header
:
t
(
"common.name"
),
</
div
>
render
:
(
_
,
provider
:
IdentityProvider
)
=>
(
<
div
className=
"flex flex-row items-center"
>
<
span
className=
"text-foreground"
>
{
provider
.
title
}
<
span
className=
"ml-2 text-sm text-muted-foreground"
>
(
{
provider
.
type
}
)
</
span
>
</
span
>
),
},
{
key
:
"actions"
,
header
:
""
,
className
:
"text-right"
,
render
:
(
_
,
provider
:
IdentityProvider
)
=>
(
<
DropdownMenu
>
<
DropdownMenu
>
<
DropdownMenuTrigger
asChild
>
<
DropdownMenuTrigger
asChild
>
<
Button
variant=
"outline
"
>
<
Button
variant=
"outline"
size=
"sm
"
>
<
MoreVerticalIcon
className=
"w-4 h-auto"
/>
<
MoreVerticalIcon
className=
"w-4 h-auto"
/>
</
Button
>
</
Button
>
</
DropdownMenuTrigger
>
</
DropdownMenuTrigger
>
<
DropdownMenuContent
align=
"end"
sideOffset=
{
2
}
>
<
DropdownMenuContent
align=
"end"
sideOffset=
{
2
}
>
<
DropdownMenuItem
onClick=
{
()
=>
handleEditIdentityProvider
(
identityProvider
)
}
>
{
t
(
"common.edit"
)
}
</
DropdownMenuItem
>
<
DropdownMenuItem
onClick=
{
()
=>
handleEditIdentityProvider
(
provider
)
}
>
{
t
(
"common.edit"
)
}
</
DropdownMenuItem
>
<
DropdownMenuItem
onClick=
{
()
=>
handleDeleteIdentityProvider
(
identityProvider
)
}
>
{
t
(
"common.delete"
)
}
</
DropdownMenuItem
>
<
DropdownMenuItem
onClick=
{
()
=>
handleDeleteIdentityProvider
(
provider
)
}
className=
"text-destructive focus:text-destructive"
>
{
t
(
"common.delete"
)
}
</
DropdownMenuItem
>
</
DropdownMenuContent
>
</
DropdownMenuContent
>
</
DropdownMenu
>
</
DropdownMenu
>
</
div
>
),
</
div
>
},
))
}
]
}
{
identityProviderList
.
length
===
0
&&
(
data=
{
identityProviderList
}
<
div
className=
"w-full mt-2 text-sm border-border text-muted-foreground flex flex-row items-center justify-between"
>
emptyMessage=
{
t
(
"setting.sso-section.no-sso-found"
)
}
<
p
className=
""
>
{
t
(
"setting.sso-section.no-sso-found"
)
}
</
p
>
getRowKey=
{
(
provider
)
=>
provider
.
name
}
</
div
>
/>
)
}
<
CreateIdentityProviderDialog
<
CreateIdentityProviderDialog
open=
{
isCreateDialogOpen
}
open=
{
isCreateDialogOpen
}
...
@@ -127,7 +140,7 @@ const SSOSection = () => {
...
@@ -127,7 +140,7 @@ const SSOSection = () => {
onConfirm=
{
confirmDeleteIdentityProvider
}
onConfirm=
{
confirmDeleteIdentityProvider
}
confirmVariant=
"destructive"
confirmVariant=
"destructive"
/>
/>
</
div
>
</
SettingSection
>
);
);
};
};
...
...
web/src/components/Settings/SettingGroup.tsx
0 → 100644
View file @
c54fcf7a
import
React
from
"react"
;
import
{
Separator
}
from
"@/components/ui/separator"
;
import
{
cn
}
from
"@/lib/utils"
;
interface
SettingGroupProps
{
title
?:
string
;
description
?:
string
;
children
:
React
.
ReactNode
;
className
?:
string
;
showSeparator
?:
boolean
;
}
/**
* Groups related settings together with optional title and separator
* Use this to organize multiple SettingRows under a common category
*/
const
SettingGroup
:
React
.
FC
<
SettingGroupProps
>
=
({
title
,
description
,
children
,
className
,
showSeparator
=
false
})
=>
{
return
(
<>
{
showSeparator
&&
<
Separator
className=
"my-2"
/>
}
<
div
className=
{
cn
(
"flex flex-col gap-3"
,
className
)
}
>
{
(
title
||
description
)
&&
(
<
div
className=
"flex flex-col gap-1"
>
{
title
&&
<
h4
className=
"text-sm font-medium text-muted-foreground"
>
{
title
}
</
h4
>
}
{
description
&&
<
p
className=
"text-xs text-muted-foreground"
>
{
description
}
</
p
>
}
</
div
>
)
}
<
div
className=
"flex flex-col gap-3"
>
{
children
}
</
div
>
</
div
>
</>
);
};
export
default
SettingGroup
;
web/src/components/Settings/SettingRow.tsx
0 → 100644
View file @
c54fcf7a
import
{
HelpCircleIcon
}
from
"lucide-react"
;
import
React
from
"react"
;
import
{
Tooltip
,
TooltipContent
,
TooltipProvider
,
TooltipTrigger
}
from
"@/components/ui/tooltip"
;
import
{
cn
}
from
"@/lib/utils"
;
interface
SettingRowProps
{
label
:
string
;
description
?:
string
;
tooltip
?:
string
;
children
:
React
.
ReactNode
;
className
?:
string
;
vertical
?:
boolean
;
}
/**
* Standardized row component for individual settings
* Provides consistent label/control layout with optional tooltip
*/
const
SettingRow
:
React
.
FC
<
SettingRowProps
>
=
({
label
,
description
,
tooltip
,
children
,
className
,
vertical
=
false
})
=>
{
return
(
<
div
className=
{
cn
(
"w-full flex gap-3"
,
vertical
?
"flex-col"
:
"flex-row justify-between items-center"
,
className
)
}
>
<
div
className=
{
cn
(
"flex flex-col gap-1"
,
vertical
?
"w-full"
:
"flex-1 min-w-0"
)
}
>
<
div
className=
"flex items-center gap-1.5"
>
<
span
className=
{
cn
(
"text-sm"
,
vertical
?
"font-medium"
:
""
)
}
>
{
label
}
</
span
>
{
tooltip
&&
(
<
TooltipProvider
>
<
Tooltip
>
<
TooltipTrigger
asChild
>
<
HelpCircleIcon
className=
"w-4 h-4 text-muted-foreground cursor-help"
/>
</
TooltipTrigger
>
<
TooltipContent
>
<
p
className=
"max-w-xs"
>
{
tooltip
}
</
p
>
</
TooltipContent
>
</
Tooltip
>
</
TooltipProvider
>
)
}
</
div
>
{
description
&&
<
p
className=
"text-xs text-muted-foreground"
>
{
description
}
</
p
>
}
</
div
>
<
div
className=
{
cn
(
"flex items-center"
,
vertical
?
"w-full"
:
"shrink-0"
)
}
>
{
children
}
</
div
>
</
div
>
);
};
export
default
SettingRow
;
web/src/components/Settings/SettingSection.tsx
0 → 100644
View file @
c54fcf7a
import
React
from
"react"
;
import
{
cn
}
from
"@/lib/utils"
;
interface
SettingSectionProps
{
title
?:
React
.
ReactNode
;
description
?:
string
;
children
:
React
.
ReactNode
;
className
?:
string
;
actions
?:
React
.
ReactNode
;
}
/**
* Wrapper component for consistent section layout in settings pages
* Provides standardized spacing, titles, and descriptions
*/
const
SettingSection
:
React
.
FC
<
SettingSectionProps
>
=
({
title
,
description
,
children
,
className
,
actions
})
=>
{
return
(
<
div
className=
{
cn
(
"w-full flex flex-col gap-4 pt-2 pb-4"
,
className
)
}
>
{
(
title
||
description
||
actions
)
&&
(
<
div
className=
"flex flex-col sm:flex-row sm:items-start sm:justify-between gap-2"
>
<
div
className=
"flex-1"
>
{
title
&&
(
<
div
className=
"text-base font-semibold text-foreground mb-1"
>
{
typeof
title
===
"string"
?
<
h3
>
{
title
}
</
h3
>
:
title
}
</
div
>
)
}
{
description
&&
<
p
className=
"text-sm text-muted-foreground"
>
{
description
}
</
p
>
}
</
div
>
{
actions
&&
<
div
className=
"flex items-center gap-2"
>
{
actions
}
</
div
>
}
</
div
>
)
}
<
div
className=
"flex flex-col gap-4"
>
{
children
}
</
div
>
</
div
>
);
};
export
default
SettingSection
;
web/src/components/Settings/SettingTable.tsx
0 → 100644
View file @
c54fcf7a
import
React
from
"react"
;
import
{
cn
}
from
"@/lib/utils"
;
interface
SettingTableColumn
{
key
:
string
;
header
:
string
;
className
?:
string
;
render
?:
(
value
:
any
,
row
:
any
)
=>
React
.
ReactNode
;
}
interface
SettingTableProps
{
columns
:
SettingTableColumn
[];
data
:
any
[];
emptyMessage
?:
string
;
className
?:
string
;
getRowKey
?:
(
row
:
any
,
index
:
number
)
=>
string
;
}
/**
* Standardized table component for settings data lists
* Provides consistent styling for tables in settings pages
*/
const
SettingTable
:
React
.
FC
<
SettingTableProps
>
=
({
columns
,
data
,
emptyMessage
=
"No data"
,
className
,
getRowKey
})
=>
{
return
(
<
div
className=
{
cn
(
"w-full overflow-x-auto"
,
className
)
}
>
<
div
className=
"inline-block min-w-full align-middle border border-border rounded-lg"
>
<
table
className=
"min-w-full divide-y divide-border"
>
<
thead
>
<
tr
className=
"text-sm font-semibold text-left text-foreground"
>
{
columns
.
map
((
column
)
=>
(
<
th
key=
{
column
.
key
}
scope=
"col"
className=
{
cn
(
"px-3 py-2"
,
column
.
className
)
}
>
{
column
.
header
}
</
th
>
))
}
</
tr
>
</
thead
>
<
tbody
className=
"divide-y divide-border"
>
{
data
.
length
===
0
?
(
<
tr
>
<
td
colSpan=
{
columns
.
length
}
className=
"px-3 py-4 text-center text-sm text-muted-foreground"
>
{
emptyMessage
}
</
td
>
</
tr
>
)
:
(
data
.
map
((
row
,
rowIndex
)
=>
{
const
rowKey
=
getRowKey
?
getRowKey
(
row
,
rowIndex
)
:
rowIndex
.
toString
();
return
(
<
tr
key=
{
rowKey
}
>
{
columns
.
map
((
column
)
=>
{
const
value
=
row
[
column
.
key
];
const
content
=
column
.
render
?
column
.
render
(
value
,
row
)
:
value
;
return
(
<
td
key=
{
column
.
key
}
className=
{
cn
(
"whitespace-nowrap px-3 py-2 text-sm text-muted-foreground"
,
column
.
className
)
}
>
{
content
}
</
td
>
);
})
}
</
tr
>
);
})
)
}
</
tbody
>
</
table
>
</
div
>
</
div
>
);
};
export
default
SettingTable
;
web/src/components/Settings/StorageSection.tsx
View file @
c54fcf7a
import
{
isEqual
}
from
"lodash-es"
;
import
{
isEqual
}
from
"lodash-es"
;
import
{
HelpCircleIcon
}
from
"lucide-react"
;
import
{
observer
}
from
"mobx-react-lite"
;
import
{
observer
}
from
"mobx-react-lite"
;
import
React
,
{
useEffect
,
useMemo
,
useState
}
from
"react"
;
import
React
,
{
useEffect
,
useMemo
,
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
{
toast
}
from
"react-hot-toast"
;
...
@@ -8,7 +7,6 @@ import { Input } from "@/components/ui/input";
...
@@ -8,7 +7,6 @@ import { Input } from "@/components/ui/input";
import
{
Label
}
from
"@/components/ui/label"
;
import
{
Label
}
from
"@/components/ui/label"
;
import
{
RadioGroup
,
RadioGroupItem
}
from
"@/components/ui/radio-group"
;
import
{
RadioGroup
,
RadioGroupItem
}
from
"@/components/ui/radio-group"
;
import
{
Switch
}
from
"@/components/ui/switch"
;
import
{
Switch
}
from
"@/components/ui/switch"
;
import
{
Tooltip
,
TooltipContent
,
TooltipProvider
,
TooltipTrigger
}
from
"@/components/ui/tooltip"
;
import
{
instanceStore
}
from
"@/store"
;
import
{
instanceStore
}
from
"@/store"
;
import
{
instanceSettingNamePrefix
}
from
"@/store/common"
;
import
{
instanceSettingNamePrefix
}
from
"@/store/common"
;
import
{
import
{
...
@@ -18,6 +16,9 @@ import {
...
@@ -18,6 +16,9 @@ import {
InstanceSetting_StorageSetting_StorageType
,
InstanceSetting_StorageSetting_StorageType
,
}
from
"@/types/proto/api/v1/instance_service"
;
}
from
"@/types/proto/api/v1/instance_service"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
SettingGroup
from
"./SettingGroup"
;
import
SettingRow
from
"./SettingRow"
;
import
SettingSection
from
"./SettingSection"
;
const
StorageSection
=
observer
(()
=>
{
const
StorageSection
=
observer
(()
=>
{
const
t
=
useTranslate
();
const
t
=
useTranslate
();
...
@@ -131,8 +132,9 @@ const StorageSection = observer(() => {
...
@@ -131,8 +132,9 @@ const StorageSection = observer(() => {
};
};
return
(
return
(
<
div
className=
"w-full flex flex-col gap-2 pt-2 pb-4"
>
<
SettingSection
>
<
div
className=
"font-medium text-muted-foreground"
>
{
t
(
"setting.storage-section.current-storage"
)
}
</
div
>
<
SettingGroup
title=
{
t
(
"setting.storage-section.current-storage"
)
}
>
<
div
className=
"w-full"
>
<
RadioGroup
<
RadioGroup
value=
{
instanceStorageSetting
.
storageType
}
value=
{
instanceStorageSetting
.
storageType
}
onValueChange=
{
(
value
)
=>
{
onValueChange=
{
(
value
)
=>
{
...
@@ -153,85 +155,66 @@ const StorageSection = observer(() => {
...
@@ -153,85 +155,66 @@ const StorageSection = observer(() => {
<
Label
htmlFor=
"s3"
>
S3
</
Label
>
<
Label
htmlFor=
"s3"
>
S3
</
Label
>
</
div
>
</
div
>
</
RadioGroup
>
</
RadioGroup
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
div
className=
"flex flex-row items-center"
>
<
span
className=
"text-muted-foreground mr-1"
>
{
t
(
"setting.system-section.max-upload-size"
)
}
</
span
>
<
TooltipProvider
>
<
Tooltip
>
<
TooltipTrigger
>
<
HelpCircleIcon
className=
"w-4 h-auto"
/>
</
TooltipTrigger
>
<
TooltipContent
>
<
p
>
{
t
(
"setting.system-section.max-upload-size-hint"
)
}
</
p
>
</
TooltipContent
>
</
Tooltip
>
</
TooltipProvider
>
</
div
>
<
Input
className=
"w-16 font-mono"
value=
{
instanceStorageSetting
.
uploadSizeLimitMb
}
onChange=
{
handleMaxUploadSizeChanged
}
/>
</
div
>
</
div
>
<
SettingRow
label=
{
t
(
"setting.system-section.max-upload-size"
)
}
tooltip=
{
t
(
"setting.system-section.max-upload-size-hint"
)
}
>
<
Input
className=
"w-24 font-mono"
value=
{
instanceStorageSetting
.
uploadSizeLimitMb
}
onChange=
{
handleMaxUploadSizeChanged
}
/>
</
SettingRow
>
{
instanceStorageSetting
.
storageType
!==
InstanceSetting_StorageSetting_StorageType
.
DATABASE
&&
(
{
instanceStorageSetting
.
storageType
!==
InstanceSetting_StorageSetting_StorageType
.
DATABASE
&&
(
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
SettingRow
label=
{
t
(
"setting.storage-section.filepath-template"
)
}
>
<
span
className=
"text-muted-foreground mr-1"
>
{
t
(
"setting.storage-section.filepath-template"
)
}
</
span
>
<
Input
<
Input
className=
"w-64"
className=
"w-64"
value=
{
instanceStorageSetting
.
filepathTemplate
}
value=
{
instanceStorageSetting
.
filepathTemplate
}
placeholder=
"assets/
{
timestamp
}
_
{
filename
}"
placeholder=
"assets/
{
timestamp
}
_
{
filename
}"
onChange=
{
handleFilepathTemplateChanged
}
onChange=
{
handleFilepathTemplateChanged
}
/>
/>
</
div
>
</
SettingRow
>
)
}
)
}
</
SettingGroup
>
{
instanceStorageSetting
.
storageType
===
InstanceSetting_StorageSetting_StorageType
.
S3
&&
(
{
instanceStorageSetting
.
storageType
===
InstanceSetting_StorageSetting_StorageType
.
S3
&&
(
<>
<
SettingGroup
title=
"S3 Configuration"
showSeparator
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
SettingRow
label=
"Access key id"
>
<
span
className=
"text-muted-foreground mr-1"
>
Access key id
</
span
>
<
Input
className=
"w-64"
value=
{
instanceStorageSetting
.
s3Config
?.
accessKeyId
}
onChange=
{
handleS3ConfigAccessKeyIdChanged
}
/>
<
Input
</
SettingRow
>
className=
"w-64"
value=
{
instanceStorageSetting
.
s3Config
?.
accessKeyId
}
<
SettingRow
label=
"Access key secret"
>
placeholder=
""
onChange=
{
handleS3ConfigAccessKeyIdChanged
}
/>
</
div
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
span
className=
"text-muted-foreground mr-1"
>
Access key secret
</
span
>
<
Input
<
Input
className=
"w-64"
className=
"w-64"
type=
"password"
value=
{
instanceStorageSetting
.
s3Config
?.
accessKeySecret
}
value=
{
instanceStorageSetting
.
s3Config
?.
accessKeySecret
}
placeholder=
""
onChange=
{
handleS3ConfigAccessKeySecretChanged
}
onChange=
{
handleS3ConfigAccessKeySecretChanged
}
/>
/>
</
div
>
</
SettingRow
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
span
className=
"text-muted-foreground mr-1"
>
Endpoint
</
span
>
<
SettingRow
label=
"Endpoint"
>
<
Input
<
Input
className=
"w-64"
value=
{
instanceStorageSetting
.
s3Config
?.
endpoint
}
onChange=
{
handleS3ConfigEndpointChanged
}
/>
className=
"w-64"
</
SettingRow
>
value=
{
instanceStorageSetting
.
s3Config
?.
endpoint
}
placeholder=
""
<
SettingRow
label=
"Region"
>
onChange=
{
handleS3ConfigEndpointChanged
}
<
Input
className=
"w-64"
value=
{
instanceStorageSetting
.
s3Config
?.
region
}
onChange=
{
handleS3ConfigRegionChanged
}
/>
/>
</
SettingRow
>
</
div
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
SettingRow
label=
"Bucket"
>
<
span
className=
"text-muted-foreground mr-1"
>
Region
</
span
>
<
Input
className=
"w-64"
value=
{
instanceStorageSetting
.
s3Config
?.
bucket
}
onChange=
{
handleS3ConfigBucketChanged
}
/>
<
Input
className=
"w-64"
value=
{
instanceStorageSetting
.
s3Config
?.
region
}
placeholder=
""
onChange=
{
handleS3ConfigRegionChanged
}
/>
</
SettingRow
>
</
div
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
SettingRow
label=
"Use Path Style"
>
<
span
className=
"text-muted-foreground mr-1"
>
Bucket
</
span
>
<
Input
className=
"w-64"
value=
{
instanceStorageSetting
.
s3Config
?.
bucket
}
placeholder=
""
onChange=
{
handleS3ConfigBucketChanged
}
/>
</
div
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
span
className=
"text-muted-foreground mr-1"
>
Use Path Style
</
span
>
<
Switch
<
Switch
checked=
{
instanceStorageSetting
.
s3Config
?.
usePathStyle
}
checked=
{
instanceStorageSetting
.
s3Config
?.
usePathStyle
}
onCheckedChange=
{
(
checked
)
=>
handleS3ConfigUsePathStyleChanged
({
target
:
{
checked
}
}
as
any
)
}
onCheckedChange=
{
(
checked
)
=>
handleS3ConfigUsePathStyleChanged
({
target
:
{
checked
}
}
as
any
)
}
/>
/>
</
div
>
</
SettingRow
>
</>
</
SettingGroup
>
)
}
)
}
<
div
>
<
div
className=
"w-full flex justify-end"
>
<
Button
disabled=
{
!
allowSaveStorageSetting
}
onClick=
{
saveInstanceStorageSetting
}
>
<
Button
disabled=
{
!
allowSaveStorageSetting
}
onClick=
{
saveInstanceStorageSetting
}
>
{
t
(
"common.save"
)
}
{
t
(
"common.save"
)
}
</
Button
>
</
Button
>
</
div
>
</
div
>
</
div
>
</
SettingSection
>
);
);
});
});
...
...
web/src/components/Settings/UserSessionsSection.tsx
View file @
c54fcf7a
...
@@ -7,6 +7,7 @@ import { userServiceClient } from "@/grpcweb";
...
@@ -7,6 +7,7 @@ import { userServiceClient } from "@/grpcweb";
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
{
UserSession
}
from
"@/types/proto/api/v1/user_service"
;
import
{
UserSession
}
from
"@/types/proto/api/v1/user_service"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
SettingTable
from
"./SettingTable"
;
const
listUserSessions
=
async
(
parent
:
string
)
=>
{
const
listUserSessions
=
async
(
parent
:
string
)
=>
{
const
{
sessions
}
=
await
userServiceClient
.
listUserSessions
({
parent
});
const
{
sessions
}
=
await
userServiceClient
.
listUserSessions
({
parent
});
...
@@ -71,87 +72,71 @@ const UserSessionsSection = () => {
...
@@ -71,87 +72,71 @@ const UserSessionsSection = () => {
};
};
return
(
return
(
<
div
className=
"mt-6 w-full flex flex-col justify-start items-start space-y-4"
>
<
div
className=
"w-full flex flex-col gap-2"
>
<
div
className=
"w-full"
>
<
div
className=
"flex flex-col gap-1"
>
<
div
className=
"sm:flex sm:items-center sm:justify-between"
>
<
h4
className=
"text-sm font-medium text-muted-foreground"
>
{
t
(
"setting.user-sessions-section.title"
)
}
</
h4
>
<
div
className=
"sm:flex-auto space-y-1"
>
<
p
className=
"text-xs text-muted-foreground"
>
{
t
(
"setting.user-sessions-section.description"
)
}
</
p
>
<
p
className=
"flex flex-row justify-start items-center font-medium text-muted-foreground"
>
{
t
(
"setting.user-sessions-section.title"
)
}
</
p
>
<
p
className=
"text-sm text-muted-foreground"
>
{
t
(
"setting.user-sessions-section.description"
)
}
</
p
>
</
div
>
</
div
>
</
div
>
<
div
className=
"w-full mt-2 flow-root"
>
<
SettingTable
<
div
className=
"overflow-x-auto"
>
columns=
{
[
<
div
className=
"inline-block min-w-full border border-border rounded-lg align-middle"
>
{
<
table
className=
"min-w-full divide-y divide-border"
>
key
:
"device"
,
<
thead
>
header
:
t
(
"setting.user-sessions-section.device"
),
<
tr
>
render
:
(
_
,
session
:
UserSession
)
=>
(
<
th
scope=
"col"
className=
"px-3 py-2 text-left text-sm font-semibold text-foreground"
>
{
t
(
"setting.user-sessions-section.device"
)
}
</
th
>
<
th
scope=
"col"
className=
"px-3 py-2 text-left text-sm font-semibold text-foreground"
>
{
t
(
"setting.user-sessions-section.last-active"
)
}
</
th
>
<
th
scope=
"col"
className=
"relative py-3.5 pl-3 pr-4"
>
<
span
className=
"sr-only"
>
{
t
(
"common.delete"
)
}
</
span
>
</
th
>
</
tr
>
</
thead
>
<
tbody
className=
"divide-y divide-border"
>
{
userSessions
.
map
((
userSession
)
=>
(
<
tr
key=
{
userSession
.
sessionId
}
>
<
td
className=
"whitespace-nowrap px-3 py-2 text-sm text-foreground"
>
<
div
className=
"flex items-center space-x-3"
>
<
div
className=
"flex items-center space-x-3"
>
{
getDeviceIcon
(
userS
ession
.
clientInfo
?.
deviceType
||
""
)
}
{
getDeviceIcon
(
s
ession
.
clientInfo
?.
deviceType
||
""
)
}
<
div
className=
"flex flex-col"
>
<
div
className=
"flex flex-col"
>
<
span
className=
"font-medium
"
>
<
span
className=
"font-medium text-foreground
"
>
{
formatDeviceInfo
(
userS
ession
.
clientInfo
)
}
{
formatDeviceInfo
(
s
ession
.
clientInfo
)
}
{
isCurrentSession
(
userS
ession
)
&&
(
{
isCurrentSession
(
s
ession
)
&&
(
<
span
className=
"ml-2 inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-primary/20 text-primary"
>
<
span
className=
"ml-2 inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-primary/20 text-primary"
>
<
WifiIcon
className=
"w-3 h-3 mr-1"
/>
<
WifiIcon
className=
"w-3 h-3 mr-1"
/>
{
t
(
"setting.user-sessions-section.current"
)
}
{
t
(
"setting.user-sessions-section.current"
)
}
</
span
>
</
span
>
)
}
)
}
</
span
>
</
span
>
<
span
className=
"text-xs text-muted-foreground font-mono"
>
{
getFormattedSessionId
(
userS
ession
.
sessionId
)
}
</
span
>
<
span
className=
"text-xs text-muted-foreground font-mono"
>
{
getFormattedSessionId
(
s
ession
.
sessionId
)
}
</
span
>
</
div
>
</
div
>
</
div
>
</
div
>
</
td
>
),
<
td
className=
"whitespace-nowrap px-3 py-2 text-sm text-muted-foreground"
>
},
{
key
:
"lastAccessedTime"
,
header
:
t
(
"setting.user-sessions-section.last-active"
),
render
:
(
_
,
session
:
UserSession
)
=>
(
<
div
className=
"flex items-center space-x-1"
>
<
div
className=
"flex items-center space-x-1"
>
<
ClockIcon
className=
"w-4 h-4"
/>
<
ClockIcon
className=
"w-4 h-4"
/>
<
span
>
{
userS
ession
.
lastAccessedTime
?.
toLocaleString
()
}
</
span
>
<
span
>
{
s
ession
.
lastAccessedTime
?.
toLocaleString
()
}
</
span
>
</
div
>
</
div
>
</
td
>
),
<
td
className=
"relative whitespace-nowrap py-2 pl-3 pr-4 text-right text-sm"
>
},
{
key
:
"actions"
,
header
:
""
,
className
:
"text-right"
,
render
:
(
_
,
session
:
UserSession
)
=>
(
<
Button
<
Button
variant=
"ghost"
variant=
"ghost"
disabled=
{
isCurrentSession
(
userSession
)
}
size=
"sm"
onClick=
{
()
=>
{
disabled=
{
isCurrentSession
(
session
)
}
handleRevokeSession
(
userSession
);
onClick=
{
()
=>
handleRevokeSession
(
session
)
}
}
}
title=
{
title=
{
isCurrentSession
(
userS
ession
)
isCurrentSession
(
s
ession
)
?
t
(
"setting.user-sessions-section.cannot-revoke-current"
)
?
t
(
"setting.user-sessions-section.cannot-revoke-current"
)
:
t
(
"setting.user-sessions-section.revoke-session"
)
:
t
(
"setting.user-sessions-section.revoke-session"
)
}
}
>
>
<
TrashIcon
<
TrashIcon
className=
{
`w-4 h-auto ${isCurrentSession(session) ? "text-muted-foreground" : "text-destructive"}`
}
/>
className=
{
`w-4 h-auto ${isCurrentSession(userSession) ? "text-muted-foreground" : "text-destructive"}`
}
/>
</
Button
>
</
Button
>
</
td
>
),
</
tr
>
},
))
}
]
}
</
tbody
>
data=
{
userSessions
}
</
table
>
emptyMessage=
{
t
(
"setting.user-sessions-section.no-sessions"
)
}
{
userSessions
.
length
===
0
&&
(
getRowKey=
{
(
session
)
=>
session
.
sessionId
}
<
div
className=
"text-center py-8 text-muted-foreground"
>
{
t
(
"setting.user-sessions-section.no-sessions"
)
}
</
div
>
/>
)
}
</
div
>
</
div
>
</
div
>
<
ConfirmDialog
<
ConfirmDialog
open=
{
!!
revokeTarget
}
open=
{
!!
revokeTarget
}
onOpenChange=
{
(
open
)
=>
!
open
&&
setRevokeTarget
(
undefined
)
}
onOpenChange=
{
(
open
)
=>
!
open
&&
setRevokeTarget
(
undefined
)
}
...
@@ -169,7 +154,6 @@ const UserSessionsSection = () => {
...
@@ -169,7 +154,6 @@ const UserSessionsSection = () => {
confirmVariant=
"destructive"
confirmVariant=
"destructive"
/>
/>
</
div
>
</
div
>
</
div
>
);
);
};
};
...
...
web/src/components/Settings/WebhookSection.tsx
View file @
c54fcf7a
import
{
ExternalLinkIcon
,
TrashIcon
}
from
"lucide-react"
;
import
{
ExternalLinkIcon
,
PlusIcon
,
TrashIcon
}
from
"lucide-react"
;
import
{
useEffect
,
useState
}
from
"react"
;
import
{
useEffect
,
useState
}
from
"react"
;
import
toast
from
"react-hot-toast"
;
import
toast
from
"react-hot-toast"
;
import
{
Link
}
from
"react-router-dom"
;
import
{
Link
}
from
"react-router-dom"
;
...
@@ -9,6 +9,7 @@ import useCurrentUser from "@/hooks/useCurrentUser";
...
@@ -9,6 +9,7 @@ import useCurrentUser from "@/hooks/useCurrentUser";
import
{
UserWebhook
}
from
"@/types/proto/api/v1/user_service"
;
import
{
UserWebhook
}
from
"@/types/proto/api/v1/user_service"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
CreateWebhookDialog
from
"../CreateWebhookDialog"
;
import
CreateWebhookDialog
from
"../CreateWebhookDialog"
;
import
SettingTable
from
"./SettingTable"
;
const
WebhookSection
=
()
=>
{
const
WebhookSection
=
()
=>
{
const
t
=
useTranslate
();
const
t
=
useTranslate
();
...
@@ -52,71 +53,58 @@ const WebhookSection = () => {
...
@@ -52,71 +53,58 @@ const WebhookSection = () => {
};
};
return
(
return
(
<
div
className=
"w-full flex flex-col justify-start items-start"
>
<
div
className=
"w-full flex flex-col gap-2"
>
<
div
className=
"w-full flex justify-between items-center"
>
<
div
className=
"flex flex-col sm:flex-row sm:items-start sm:justify-between gap-2"
>
<
div
className=
"flex-auto space-y-1"
>
<
h4
className=
"text-sm font-medium text-muted-foreground"
>
{
t
(
"setting.webhook-section.title"
)
}
</
h4
>
<
p
className=
"flex flex-row justify-start items-center font-medium text-muted-foreground"
>
{
t
(
"setting.webhook-section.title"
)
}
</
p
>
<
Button
onClick=
{
()
=>
setIsCreateWebhookDialogOpen
(
true
)
}
size=
"sm"
>
</
div
>
<
PlusIcon
className=
"w-4 h-4 mr-1.5"
/>
<
div
>
<
Button
color=
"primary"
onClick=
{
()
=>
setIsCreateWebhookDialogOpen
(
true
)
}
>
{
t
(
"common.create"
)
}
{
t
(
"common.create"
)
}
</
Button
>
</
Button
>
</
div
>
</
div
>
</
div
>
<
div
className=
"w-full mt-2 flow-root"
>
<
SettingTable
<
div
className=
"overflow-x-auto"
>
columns=
{
[
<
div
className=
"inline-block min-w-full border border-border rounded-lg align-middle"
>
{
<
table
className=
"min-w-full divide-y divide-border"
>
key
:
"displayName"
,
<
thead
>
header
:
t
(
"common.name"
),
<
tr
>
render
:
(
_
,
webhook
:
UserWebhook
)
=>
<
span
className=
"text-foreground"
>
{
webhook
.
displayName
}
</
span
>,
<
th
scope=
"col"
className=
"px-3 py-2 text-left text-sm font-semibold text-foreground"
>
},
{
t
(
"common.name"
)
}
{
</
th
>
key
:
"url"
,
<
th
scope=
"col"
className=
"px-3 py-2 text-left text-sm font-semibold text-foreground"
>
header
:
t
(
"setting.webhook-section.url"
),
{
t
(
"setting.webhook-section.url"
)
}
render
:
(
_
,
webhook
:
UserWebhook
)
=>
(
</
th
>
<
span
className=
"max-w-[300px] inline-block truncate text-foreground"
title=
{
webhook
.
url
}
>
<
th
scope=
"col"
className=
"relative px-3 py-2 pr-4"
>
<
span
className=
"sr-only"
>
{
t
(
"common.delete"
)
}
</
span
>
</
th
>
</
tr
>
</
thead
>
<
tbody
className=
"divide-y divide-border"
>
{
webhooks
.
map
((
webhook
)
=>
(
<
tr
key=
{
webhook
.
name
}
>
<
td
className=
"whitespace-nowrap px-3 py-2 text-sm text-foreground"
>
{
webhook
.
displayName
}
</
td
>
<
td
className=
"max-w-[200px] px-3 py-2 text-sm text-foreground truncate"
title=
{
webhook
.
url
}
>
{
webhook
.
url
}
{
webhook
.
url
}
</
td
>
</
span
>
<
td
className=
"relative whitespace-nowrap px-3 py-2 text-right text-sm"
>
),
<
Button
variant=
"ghost"
onClick=
{
()
=>
handleDeleteWebhook
(
webhook
)
}
>
},
{
key
:
"actions"
,
header
:
""
,
className
:
"text-right"
,
render
:
(
_
,
webhook
:
UserWebhook
)
=>
(
<
Button
variant=
"ghost"
size=
"sm"
onClick=
{
()
=>
handleDeleteWebhook
(
webhook
)
}
>
<
TrashIcon
className=
"text-destructive w-4 h-auto"
/>
<
TrashIcon
className=
"text-destructive w-4 h-auto"
/>
</
Button
>
</
Button
>
</
td
>
),
</
tr
>
},
))
}
]
}
data=
{
webhooks
}
emptyMessage=
{
t
(
"setting.webhook-section.no-webhooks-found"
)
}
getRowKey=
{
(
webhook
)
=>
webhook
.
name
}
/>
{
webhooks
.
length
===
0
&&
(
<
div
className=
"w-full"
>
<
tr
>
<
td
className=
"whitespace-nowrap px-3 py-2 text-sm text-foreground"
colSpan=
{
3
}
>
{
t
(
"setting.webhook-section.no-webhooks-found"
)
}
</
td
>
</
tr
>
)
}
</
tbody
>
</
table
>
</
div
>
</
div
>
</
div
>
<
div
className=
"w-full mt-2"
>
<
Link
<
Link
className=
"text-muted-foreground text-sm inline-flex
flex-row justify-start
items-center hover:underline hover:text-primary"
className=
"text-muted-foreground text-sm inline-flex items-center hover:underline hover:text-primary"
to=
"https://www.usememos.com/docs/integrations/webhooks"
to=
"https://www.usememos.com/docs/integrations/webhooks"
target=
"_blank"
target=
"_blank"
>
>
{
t
(
"common.learn-more"
)
}
{
t
(
"common.learn-more"
)
}
<
ExternalLinkIcon
className=
"
inline w-4 h-auto
ml-1"
/>
<
ExternalLinkIcon
className=
"
w-4 h-4
ml-1"
/>
</
Link
>
</
Link
>
</
div
>
</
div
>
<
CreateWebhookDialog
<
CreateWebhookDialog
open=
{
isCreateWebhookDialogOpen
}
open=
{
isCreateWebhookDialogOpen
}
onOpenChange=
{
setIsCreateWebhookDialogOpen
}
onOpenChange=
{
setIsCreateWebhookDialogOpen
}
...
...
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