Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
D
dify
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
ai-tech
dify
Commits
d3f8ea2d
Unverified
Commit
d3f8ea2d
authored
Aug 30, 2023
by
Matri
Committed by
GitHub
Aug 30, 2023
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Feat/support to invite multiple users (#1011)
parent
7df56ed6
Changes
12
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
221 additions
and
69 deletions
+221
-69
members.py
api/controllers/console/workspace/members.py
+26
-29
index.tsx
.../components/header/account-setting/members-page/index.tsx
+6
-5
index.module.css
...ccount-setting/members-page/invite-modal/index.module.css
+8
-0
index.tsx
...eader/account-setting/members-page/invite-modal/index.tsx
+102
-19
index.tsx
...ader/account-setting/members-page/invited-modal/index.tsx
+44
-6
invitation-link.tsx
...nt-setting/members-page/invited-modal/invitation-link.tsx
+5
-4
common.en.ts
web/i18n/lang/common.en.ts
+4
-2
common.zh.ts
web/i18n/lang/common.zh.ts
+3
-1
common.ts
web/models/common.ts
+14
-0
package.json
web/package.json
+1
-0
common.ts
web/service/common.ts
+3
-3
yarn.lock
web/yarn.lock
+5
-0
No files found.
api/controllers/console/workspace/members.py
View file @
d3f8ea2d
...
@@ -49,46 +49,43 @@ class MemberInviteEmailApi(Resource):
...
@@ -49,46 +49,43 @@ class MemberInviteEmailApi(Resource):
@
account_initialization_required
@
account_initialization_required
def
post
(
self
):
def
post
(
self
):
parser
=
reqparse
.
RequestParser
()
parser
=
reqparse
.
RequestParser
()
parser
.
add_argument
(
'email
'
,
type
=
str
,
required
=
True
,
location
=
'json
'
)
parser
.
add_argument
(
'email
s'
,
type
=
str
,
required
=
True
,
location
=
'json'
,
action
=
'append
'
)
parser
.
add_argument
(
'role'
,
type
=
str
,
required
=
True
,
default
=
'admin'
,
location
=
'json'
)
parser
.
add_argument
(
'role'
,
type
=
str
,
required
=
True
,
default
=
'admin'
,
location
=
'json'
)
args
=
parser
.
parse_args
()
args
=
parser
.
parse_args
()
invitee_email
=
args
[
'email
'
]
invitee_email
s
=
args
[
'emails
'
]
invitee_role
=
args
[
'role'
]
invitee_role
=
args
[
'role'
]
if
invitee_role
not
in
[
'admin'
,
'normal'
]:
if
invitee_role
not
in
[
'admin'
,
'normal'
]:
return
{
'code'
:
'invalid-role'
,
'message'
:
'Invalid role'
},
400
return
{
'code'
:
'invalid-role'
,
'message'
:
'Invalid role'
},
400
inviter
=
current_user
inviter
=
current_user
invitation_results
=
[]
try
:
console_web_url
=
current_app
.
config
.
get
(
"CONSOLE_WEB_URL"
)
token
=
RegisterService
.
invite_new_member
(
inviter
.
current_tenant
,
invitee_email
,
role
=
invitee_role
,
for
invitee_email
in
invitee_emails
:
inviter
=
inviter
)
try
:
account
=
db
.
session
.
query
(
Account
,
TenantAccountJoin
.
role
)
.
join
(
token
=
RegisterService
.
invite_new_member
(
inviter
.
current_tenant
,
invitee_email
,
role
=
invitee_role
,
TenantAccountJoin
,
Account
.
id
==
TenantAccountJoin
.
account_id
inviter
=
inviter
)
)
.
filter
(
Account
.
email
==
args
[
'email'
])
.
first
()
account
=
db
.
session
.
query
(
Account
,
TenantAccountJoin
.
role
)
.
join
(
account
,
role
=
account
TenantAccountJoin
,
Account
.
id
==
TenantAccountJoin
.
account_id
account
=
marshal
(
account
,
account_fields
)
)
.
filter
(
Account
.
email
==
invitee_email
)
.
first
()
account
[
'role'
]
=
role
account
,
role
=
account
except
services
.
errors
.
account
.
CannotOperateSelfError
as
e
:
invitation_results
.
append
({
return
{
'code'
:
'cannot-operate-self'
,
'message'
:
str
(
e
)},
400
'status'
:
'success'
,
except
services
.
errors
.
account
.
NoPermissionError
as
e
:
'email'
:
invitee_email
,
return
{
'code'
:
'forbidden'
,
'message'
:
str
(
e
)},
403
'url'
:
f
'{console_web_url}/activate?workspace_id={current_user.current_tenant_id}&email={invitee_email}&token={token}'
except
services
.
errors
.
account
.
AccountAlreadyInTenantError
as
e
:
})
return
{
'code'
:
'email-taken'
,
'message'
:
str
(
e
)},
409
account
=
marshal
(
account
,
account_fields
)
except
Exception
as
e
:
account
[
'role'
]
=
role
return
{
'code'
:
'unexpected-error'
,
'message'
:
str
(
e
)},
500
except
Exception
as
e
:
invitation_results
.
append
({
# todo:413
'status'
:
'failed'
,
'email'
:
invitee_email
,
'message'
:
str
(
e
)
})
return
{
return
{
'result'
:
'success'
,
'result'
:
'success'
,
'account'
:
account
,
'invitation_results'
:
invitation_results
,
'invite_url'
:
'{}/activate?workspace_id={}&email={}&token={}'
.
format
(
current_app
.
config
.
get
(
"CONSOLE_WEB_URL"
),
str
(
current_user
.
current_tenant_id
),
invitee_email
,
token
)
},
201
},
201
...
...
web/app/components/header/account-setting/members-page/index.tsx
View file @
d3f8ea2d
...
@@ -16,6 +16,7 @@ import { fetchMembers } from '@/service/common'
...
@@ -16,6 +16,7 @@ import { fetchMembers } from '@/service/common'
import
I18n
from
'@/context/i18n'
import
I18n
from
'@/context/i18n'
import
{
useAppContext
}
from
'@/context/app-context'
import
{
useAppContext
}
from
'@/context/app-context'
import
Avatar
from
'@/app/components/base/avatar'
import
Avatar
from
'@/app/components/base/avatar'
import
type
{
InvitationResult
}
from
'@/models/common'
dayjs
.
extend
(
relativeTime
)
dayjs
.
extend
(
relativeTime
)
...
@@ -30,7 +31,7 @@ const MembersPage = () => {
...
@@ -30,7 +31,7 @@ const MembersPage = () => {
const
{
userProfile
,
currentWorkspace
,
isCurrentWorkspaceManager
}
=
useAppContext
()
const
{
userProfile
,
currentWorkspace
,
isCurrentWorkspaceManager
}
=
useAppContext
()
const
{
data
,
mutate
}
=
useSWR
({
url
:
'/workspaces/current/members'
},
fetchMembers
)
const
{
data
,
mutate
}
=
useSWR
({
url
:
'/workspaces/current/members'
},
fetchMembers
)
const
[
inviteModalVisible
,
setInviteModalVisible
]
=
useState
(
false
)
const
[
inviteModalVisible
,
setInviteModalVisible
]
=
useState
(
false
)
const
[
invitation
Link
,
setInvitationLink
]
=
useState
(
''
)
const
[
invitation
Results
,
setInvitationResults
]
=
useState
<
InvitationResult
[]
>
([]
)
const
[
invitedModalVisible
,
setInvitedModalVisible
]
=
useState
(
false
)
const
[
invitedModalVisible
,
setInvitedModalVisible
]
=
useState
(
false
)
const
accounts
=
data
?.
accounts
||
[]
const
accounts
=
data
?.
accounts
||
[]
const
owner
=
accounts
.
filter
(
account
=>
account
.
role
===
'owner'
)?.[
0
]?.
email
===
userProfile
.
email
const
owner
=
accounts
.
filter
(
account
=>
account
.
role
===
'owner'
)?.[
0
]?.
email
===
userProfile
.
email
...
@@ -78,7 +79,7 @@ const MembersPage = () => {
...
@@ -78,7 +79,7 @@ const MembersPage = () => {
<
div
className=
'shrink-0 w-[96px] flex items-center'
>
<
div
className=
'shrink-0 w-[96px] flex items-center'
>
{
{
(
owner
&&
account
.
role
!==
'owner'
)
(
owner
&&
account
.
role
!==
'owner'
)
?
<
Operation
member=
{
account
}
onOperate=
{
()
=>
mutate
()
}
/>
?
<
Operation
member=
{
account
}
onOperate=
{
mutate
}
/>
:
<
div
className=
'px-3 text-[13px] text-gray-700'
>
{
RoleMap
[
account
.
role
]
||
RoleMap
.
normal
}
</
div
>
:
<
div
className=
'px-3 text-[13px] text-gray-700'
>
{
RoleMap
[
account
.
role
]
||
RoleMap
.
normal
}
</
div
>
}
}
</
div
>
</
div
>
...
@@ -92,9 +93,9 @@ const MembersPage = () => {
...
@@ -92,9 +93,9 @@ const MembersPage = () => {
inviteModalVisible
&&
(
inviteModalVisible
&&
(
<
InviteModal
<
InviteModal
onCancel=
{
()
=>
setInviteModalVisible
(
false
)
}
onCancel=
{
()
=>
setInviteModalVisible
(
false
)
}
onSend=
{
(
url
)
=>
{
onSend=
{
(
invitationResults
)
=>
{
setInvitedModalVisible
(
true
)
setInvitedModalVisible
(
true
)
setInvitation
Link
(
url
)
setInvitation
Results
(
invitationResults
)
mutate
()
mutate
()
}
}
}
}
/>
/>
...
@@ -103,7 +104,7 @@ const MembersPage = () => {
...
@@ -103,7 +104,7 @@ const MembersPage = () => {
{
{
invitedModalVisible
&&
(
invitedModalVisible
&&
(
<
InvitedModal
<
InvitedModal
invitation
Link=
{
invitationLink
}
invitation
Results=
{
invitationResults
}
onCancel=
{
()
=>
setInvitedModalVisible
(
false
)
}
onCancel=
{
()
=>
setInvitedModalVisible
(
false
)
}
/>
/>
)
)
...
...
web/app/components/header/account-setting/members-page/invite-modal/index.module.css
View file @
d3f8ea2d
.modal
{
.modal
{
padding
:
24px
32px
!important
;
padding
:
24px
32px
!important
;
width
:
400px
!important
;
width
:
400px
!important
;
}
.emailsInput
{
background-color
:
rgb
(
243
244
246
/
var
(
--tw-bg-opacity
))
!important
;
}
.emailBackground
{
background-color
:
white
!important
;
}
}
\ No newline at end of file
web/app/components/header/account-setting/members-page/invite-modal/index.tsx
View file @
d3f8ea2d
'use client'
'use client'
import
{
useState
}
from
'react'
import
{
Fragment
,
useCallback
,
useMemo
,
useState
}
from
'react'
import
{
useContext
}
from
'use-context-selector'
import
{
useContext
}
from
'use-context-selector'
import
{
XMarkIcon
}
from
'@heroicons/react/24/outline'
import
{
XMarkIcon
}
from
'@heroicons/react/24/outline'
import
{
useTranslation
}
from
'react-i18next'
import
{
useTranslation
}
from
'react-i18next'
import
{
ReactMultiEmail
}
from
'react-multi-email'
import
{
Listbox
,
Transition
}
from
'@headlessui/react'
import
{
CheckIcon
}
from
'@heroicons/react/20/solid'
import
cn
from
'classnames'
import
s
from
'./index.module.css'
import
s
from
'./index.module.css'
import
Modal
from
'@/app/components/base/modal'
import
Modal
from
'@/app/components/base/modal'
import
Button
from
'@/app/components/base/button'
import
Button
from
'@/app/components/base/button'
import
{
inviteMember
}
from
'@/service/common'
import
{
inviteMember
}
from
'@/service/common'
import
{
emailRegex
}
from
'@/config'
import
{
emailRegex
}
from
'@/config'
import
{
ToastContext
}
from
'@/app/components/base/toast'
import
{
ToastContext
}
from
'@/app/components/base/toast'
import
type
{
InvitationResult
}
from
'@/models/common'
import
'react-multi-email/dist/style.css'
type
IInviteModalProps
=
{
type
IInviteModalProps
=
{
onCancel
:
()
=>
void
onCancel
:
()
=>
void
onSend
:
(
url
:
string
)
=>
void
onSend
:
(
invitationResults
:
InvitationResult
[]
)
=>
void
}
}
const
InviteModal
=
({
const
InviteModal
=
({
onCancel
,
onCancel
,
onSend
,
onSend
,
}:
IInviteModalProps
)
=>
{
}:
IInviteModalProps
)
=>
{
const
{
t
}
=
useTranslation
()
const
{
t
}
=
useTranslation
()
const
[
email
,
setEmail
]
=
useState
(
''
)
const
[
email
s
,
setEmails
]
=
useState
<
string
[]
>
([]
)
const
{
notify
}
=
useContext
(
ToastContext
)
const
{
notify
}
=
useContext
(
ToastContext
)
const
handleSend
=
async
()
=>
{
const
InvitingRoles
=
useMemo
(()
=>
[
if
(
emailRegex
.
test
(
email
))
{
{
name
:
'normal'
,
description
:
t
(
'common.members.normalTip'
),
},
{
name
:
'admin'
,
description
:
t
(
'common.members.adminTip'
),
},
],
[
t
])
const
[
role
,
setRole
]
=
useState
(
InvitingRoles
[
0
])
const
handleSend
=
useCallback
(
async
()
=>
{
if
(
emails
.
map
(
email
=>
emailRegex
.
test
(
email
)).
every
(
Boolean
))
{
try
{
try
{
const
res
=
await
inviteMember
({
url
:
'/workspaces/current/members/invite-email'
,
body
:
{
email
,
role
:
'admin'
}
})
const
{
result
,
invitation_results
}
=
await
inviteMember
({
url
:
'/workspaces/current/members/invite-email'
,
body
:
{
emails
,
role
:
role
.
name
},
})
if
(
res
.
res
ult
===
'success'
)
{
if
(
result
===
'success'
)
{
onCancel
()
onCancel
()
onSend
(
res
.
invite_url
)
onSend
(
invitation_results
)
}
}
}
}
catch
(
e
)
{}
catch
(
e
)
{}
...
@@ -37,11 +59,11 @@ const InviteModal = ({
...
@@ -37,11 +59,11 @@ const InviteModal = ({
else
{
else
{
notify
({
type
:
'error'
,
message
:
t
(
'common.members.emailInvalid'
)
})
notify
({
type
:
'error'
,
message
:
t
(
'common.members.emailInvalid'
)
})
}
}
}
}
,
[
role
,
emails
,
notify
,
onCancel
,
onSend
,
t
])
return
(
return
(
<
div
className=
{
s
.
wrap
}
>
<
div
className=
{
s
.
wrap
}
>
<
Modal
isShow
onClose=
{
()
=>
{}
}
className=
{
s
.
modal
}
>
<
Modal
overflowVisible
isShow
onClose=
{
()
=>
{}
}
className=
{
s
.
modal
}
>
<
div
className=
'flex justify-between mb-2'
>
<
div
className=
'flex justify-between mb-2'
>
<
div
className=
'text-xl font-semibold text-gray-900'
>
{
t
(
'common.members.inviteTeamMember'
)
}
</
div
>
<
div
className=
'text-xl font-semibold text-gray-900'
>
{
t
(
'common.members.inviteTeamMember'
)
}
</
div
>
<
XMarkIcon
className=
'w-4 h-4 cursor-pointer'
onClick=
{
onCancel
}
/>
<
XMarkIcon
className=
'w-4 h-4 cursor-pointer'
onClick=
{
onCancel
}
/>
...
@@ -49,18 +71,79 @@ const InviteModal = ({
...
@@ -49,18 +71,79 @@ const InviteModal = ({
<
div
className=
'mb-7 text-[13px] text-gray-500'
>
{
t
(
'common.members.inviteTeamMemberTip'
)
}
</
div
>
<
div
className=
'mb-7 text-[13px] text-gray-500'
>
{
t
(
'common.members.inviteTeamMemberTip'
)
}
</
div
>
<
div
>
<
div
>
<
div
className=
'mb-2 text-sm font-medium text-gray-900'
>
{
t
(
'common.members.email'
)
}
</
div
>
<
div
className=
'mb-2 text-sm font-medium text-gray-900'
>
{
t
(
'common.members.email'
)
}
</
div
>
<
input
<
div
className=
'mb-8 h-36 flex items-stretch'
>
className=
'
<
ReactMultiEmail
block w-full py-2 mb-9 px-3 bg-gray-50 outline-none border-none
className=
{
cn
(
'w-full pt-2 px-3 outline-none border-none'
,
appearance-none text-sm text-gray-900 rounded-lg
'appearance-none text-sm text-gray-900 rounded-lg overflow-y-auto'
,
'
s
.
emailsInput
,
value=
{
email
}
)
}
onChange=
{
e
=>
setEmail
(
e
.
target
.
value
)
}
autoFocus
placeholder=
{
t
(
'common.members.emailPlaceholder'
)
||
''
}
emails=
{
emails
}
/>
inputClassName=
'bg-transparent'
onChange=
{
setEmails
}
getLabel=
{
(
email
,
index
,
removeEmail
)
=>
<
div
data
-
tag
key=
{
index
}
className=
{
cn
(
s
.
emailBackground
)
}
>
<
div
data
-
tag
-
item
>
{
email
}
</
div
>
<
span
data
-
tag
-
handle
onClick=
{
()
=>
removeEmail
(
index
)
}
>
×
</
span
>
</
div
>
}
placeholder=
{
t
(
'common.members.emailPlaceholder'
)
||
''
}
/>
</
div
>
<
Listbox
value=
{
role
}
onChange=
{
setRole
}
>
<
div
className=
"relative pb-6"
>
<
Listbox
.
Button
className=
"relative w-full py-2 pl-3 pr-10 text-left bg-gray-100 outline-none border-none appearance-none text-sm text-gray-900 rounded-lg"
>
<
span
className=
"block truncate capitalize"
>
{
t
(
'common.members.invitedAsRole'
,
{
role
:
t
(
`common.members.${role.name}`
)
})
}
</
span
>
</
Listbox
.
Button
>
<
Transition
as=
{
Fragment
}
leave=
"transition ease-in duration-200"
leaveFrom=
"opacity-200"
leaveTo=
"opacity-0"
>
<
Listbox
.
Options
className=
"absolute w-full py-1 my-2 overflow-auto text-base bg-white rounded-md shadow-lg max-h-60 ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
>
{
InvitingRoles
.
map
(
role
=>
<
Listbox
.
Option
key=
{
role
.
name
}
className=
{
({
active
})
=>
`${active ? ' bg-gray-50 rounded-xl' : ' bg-transparent'}
cursor-default select-none relative py-2 px-4 mx-2 flex flex-col`
}
value=
{
role
}
>
{
({
selected
})
=>
(
<
div
className=
'flex flex-row'
>
<
span
className=
{
cn
(
'text-indigo-600 w-8'
,
'flex items-center'
,
)
}
>
{
selected
&&
(<
CheckIcon
className=
"h-5 w-5"
aria
-
hidden=
"true"
/>)
}
</
span
>
<
div
className=
' flex flex-col flex-grow'
>
<
span
className=
{
`${selected ? 'font-medium' : 'font-normal'} capitalize block truncate`
}
>
{
t
(
`common.members.${role.name}`
)
}
</
span
>
<
span
className=
{
`${selected ? 'font-medium' : 'font-normal'} capitalize block truncate`
}
>
{
role
.
description
}
</
span
>
</
div
>
</
div
>
)
}
</
Listbox
.
Option
>,
)
}
</
Listbox
.
Options
>
</
Transition
>
</
div
>
</
Listbox
>
<
Button
<
Button
tabIndex=
{
0
}
className=
'w-full text-sm font-medium'
className=
'w-full text-sm font-medium'
onClick=
{
handleSend
}
onClick=
{
handleSend
}
disabled=
{
!
emails
.
length
}
type=
'primary'
type=
'primary'
>
>
{
t
(
'common.members.sendInvite'
)
}
{
t
(
'common.members.sendInvite'
)
}
...
...
web/app/components/header/account-setting/members-page/invited-modal/index.tsx
View file @
d3f8ea2d
import
{
CheckCircleIcon
}
from
'@heroicons/react/24/solid'
import
{
CheckCircleIcon
}
from
'@heroicons/react/24/solid'
import
{
XMarkIcon
}
from
'@heroicons/react/24/outline'
import
{
QuestionMarkCircleIcon
,
XMarkIcon
}
from
'@heroicons/react/24/outline'
import
{
useTranslation
}
from
'react-i18next'
import
{
useTranslation
}
from
'react-i18next'
import
{
useMemo
}
from
'react'
import
InvitationLink
from
'./invitation-link'
import
InvitationLink
from
'./invitation-link'
import
s
from
'./index.module.css'
import
s
from
'./index.module.css'
import
Modal
from
'@/app/components/base/modal'
import
Modal
from
'@/app/components/base/modal'
import
Button
from
'@/app/components/base/button'
import
Button
from
'@/app/components/base/button'
import
{
IS_CE_EDITION
}
from
'@/config'
import
{
IS_CE_EDITION
}
from
'@/config'
import
type
{
InvitationResult
}
from
'@/models/common'
import
Tooltip
from
'@/app/components/base/tooltip'
export
type
SuccessInvationResult
=
Extract
<
InvitationResult
,
{
status
:
'success'
}
>
export
type
FailedInvationResult
=
Extract
<
InvitationResult
,
{
status
:
'failed'
}
>
type
IInvitedModalProps
=
{
type
IInvitedModalProps
=
{
invitation
Link
:
string
invitation
Results
:
InvitationResult
[]
onCancel
:
()
=>
void
onCancel
:
()
=>
void
}
}
const
InvitedModal
=
({
const
InvitedModal
=
({
invitation
Link
,
invitation
Results
,
onCancel
,
onCancel
,
}:
IInvitedModalProps
)
=>
{
}:
IInvitedModalProps
)
=>
{
const
{
t
}
=
useTranslation
()
const
{
t
}
=
useTranslation
()
const
successInvationResults
=
useMemo
<
SuccessInvationResult
[]
>
(()
=>
invitationResults
?.
filter
(
item
=>
item
.
status
===
'success'
)
as
SuccessInvationResult
[],
[
invitationResults
])
const
failedInvationResults
=
useMemo
<
FailedInvationResult
[]
>
(()
=>
invitationResults
?.
filter
(
item
=>
item
.
status
!==
'success'
)
as
FailedInvationResult
[],
[
invitationResults
])
return
(
return
(
<
div
className=
{
s
.
wrap
}
>
<
div
className=
{
s
.
wrap
}
>
<
Modal
isShow
onClose=
{
()
=>
{}
}
className=
{
s
.
modal
}
>
<
Modal
isShow
onClose=
{
()
=>
{}
}
className=
{
s
.
modal
}
>
...
@@ -37,9 +46,38 @@ const InvitedModal = ({
...
@@ -37,9 +46,38 @@ const InvitedModal = ({
{
IS_CE_EDITION
&&
(
{
IS_CE_EDITION
&&
(
<>
<>
<
div
className=
'mb-5 text-sm text-gray-500'
>
{
t
(
'common.members.invitationSentTip'
)
}
</
div
>
<
div
className=
'mb-5 text-sm text-gray-500'
>
{
t
(
'common.members.invitationSentTip'
)
}
</
div
>
<
div
className=
'mb-9'
>
<
div
className=
'flex flex-col gap-2 mb-9'
>
<
div
className=
'py-2 text-sm font-Medium text-gray-900'
>
{
t
(
'common.members.invitationLink'
)
}
</
div
>
{
<
InvitationLink
value=
{
invitationLink
}
/>
!!
successInvationResults
.
length
&&
<>
<
div
className=
'py-2 text-sm font-Medium text-gray-900'
>
{
t
(
'common.members.invitationLink'
)
}
</
div
>
{
successInvationResults
.
map
(
item
=>
<
InvitationLink
key=
{
item
.
email
}
value=
{
item
}
/>)
}
</>
}
{
!!
failedInvationResults
.
length
&&
<>
<
div
className=
'py-2 text-sm font-Medium text-gray-900'
>
{
t
(
'common.members.failedinvitationEmails'
)
}
</
div
>
<
div
className=
'flex flex-wrap justify-between gap-y-1'
>
{
failedInvationResults
.
map
(
item
=>
<
div
key=
{
item
.
email
}
className=
'flex justify-center border border-red-300 rounded-md px-1 bg-orange-50'
>
<
Tooltip
selector=
{
`invitation-tag-${item.email}`
}
htmlContent=
{
item
.
message
}
>
<
div
className=
'flex justify-center items-center text-sm gap-1'
>
{
item
.
email
}
<
QuestionMarkCircleIcon
className=
'w-4 h-4 text-red-300'
/>
</
div
>
</
Tooltip
>
</
div
>,
)
}
</
div
>
</>
}
</
div
>
</
div
>
</>
</>
)
}
)
}
...
...
web/app/components/header/account-setting/members-page/invited-modal/invitation-link.tsx
View file @
d3f8ea2d
...
@@ -3,21 +3,22 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'
...
@@ -3,21 +3,22 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'
import
{
t
}
from
'i18next'
import
{
t
}
from
'i18next'
import
copy
from
'copy-to-clipboard'
import
copy
from
'copy-to-clipboard'
import
s
from
'./index.module.css'
import
s
from
'./index.module.css'
import
type
{
SuccessInvationResult
}
from
'.'
import
Tooltip
from
'@/app/components/base/tooltip'
import
Tooltip
from
'@/app/components/base/tooltip'
import
{
randomString
}
from
'@/utils'
import
{
randomString
}
from
'@/utils'
type
IInvitationLinkProps
=
{
type
IInvitationLinkProps
=
{
value
?:
string
value
:
SuccessInvationResult
}
}
const
InvitationLink
=
({
const
InvitationLink
=
({
value
=
''
,
value
,
}:
IInvitationLinkProps
)
=>
{
}:
IInvitationLinkProps
)
=>
{
const
[
isCopied
,
setIsCopied
]
=
useState
(
false
)
const
[
isCopied
,
setIsCopied
]
=
useState
(
false
)
const
selector
=
useRef
(
`invite-link-
${
randomString
(
4
)}
`
)
const
selector
=
useRef
(
`invite-link-
${
randomString
(
4
)}
`
)
const
copyHandle
=
useCallback
(()
=>
{
const
copyHandle
=
useCallback
(()
=>
{
copy
(
value
)
copy
(
value
.
url
)
setIsCopied
(
true
)
setIsCopied
(
true
)
},
[
value
])
},
[
value
])
...
@@ -42,7 +43,7 @@ const InvitationLink = ({
...
@@ -42,7 +43,7 @@ const InvitationLink = ({
content=
{
isCopied
?
`${t('appApi.copied')}`
:
`${t('appApi.copy')}`
}
content=
{
isCopied
?
`${t('appApi.copied')}`
:
`${t('appApi.copy')}`
}
className=
'z-10'
className=
'z-10'
>
>
<
div
className=
'absolute top-0 left-0 w-full pl-2 pr-2 truncate cursor-pointer r-0'
onClick=
{
copyHandle
}
>
{
value
}
</
div
>
<
div
className=
'absolute top-0 left-0 w-full pl-2 pr-2 truncate cursor-pointer r-0'
onClick=
{
copyHandle
}
>
{
value
.
url
}
</
div
>
</
Tooltip
>
</
Tooltip
>
</
div
>
</
div
>
<
div
className=
"flex-shrink-0 h-4 bg-gray-200 border"
/>
<
div
className=
"flex-shrink-0 h-4 bg-gray-200 border"
/>
...
...
web/i18n/lang/common.en.ts
View file @
d3f8ea2d
...
@@ -135,11 +135,13 @@ const translation = {
...
@@ -135,11 +135,13 @@ const translation = {
inviteTeamMemberTip
:
'They can access your team data directly after signing in.'
,
inviteTeamMemberTip
:
'They can access your team data directly after signing in.'
,
email
:
'Email'
,
email
:
'Email'
,
emailInvalid
:
'Invalid Email Format'
,
emailInvalid
:
'Invalid Email Format'
,
emailPlaceholder
:
'Input Email'
,
emailPlaceholder
:
'Please input emails'
,
sendInvite
:
'Add'
,
sendInvite
:
'Send Invite'
,
invitedAsRole
:
'Invited as {{role}} user'
,
invitationSent
:
'Invitation sent'
,
invitationSent
:
'Invitation sent'
,
invitationSentTip
:
'Invitation sent, and they can sign in to Dify to access your team data.'
,
invitationSentTip
:
'Invitation sent, and they can sign in to Dify to access your team data.'
,
invitationLink
:
'Invitation Link'
,
invitationLink
:
'Invitation Link'
,
failedinvitationEmails
:
'Below users were not invited successfully'
,
ok
:
'OK'
,
ok
:
'OK'
,
removeFromTeam
:
'Remove from team'
,
removeFromTeam
:
'Remove from team'
,
removeFromTeamTip
:
'Will remove team access'
,
removeFromTeamTip
:
'Will remove team access'
,
...
...
web/i18n/lang/common.zh.ts
View file @
d3f8ea2d
...
@@ -136,10 +136,12 @@ const translation = {
...
@@ -136,10 +136,12 @@ const translation = {
email
:
'邮箱'
,
email
:
'邮箱'
,
emailInvalid
:
'邮箱格式无效'
,
emailInvalid
:
'邮箱格式无效'
,
emailPlaceholder
:
'输入邮箱'
,
emailPlaceholder
:
'输入邮箱'
,
sendInvite
:
'添加'
,
sendInvite
:
'发送邀请'
,
invitedAsRole
:
'邀请为{{role}}用户'
,
invitationSent
:
'邀请已发送'
,
invitationSent
:
'邀请已发送'
,
invitationSentTip
:
'邀请已发送,对方登录 Dify 后即可访问你的团队数据。'
,
invitationSentTip
:
'邀请已发送,对方登录 Dify 后即可访问你的团队数据。'
,
invitationLink
:
'邀请链接'
,
invitationLink
:
'邀请链接'
,
failedinvitationEmails
:
'邀请以下邮箱失败'
,
ok
:
'好的'
,
ok
:
'好的'
,
removeFromTeam
:
'移除团队'
,
removeFromTeam
:
'移除团队'
,
removeFromTeamTip
:
'将取消团队访问'
,
removeFromTeamTip
:
'将取消团队访问'
,
...
...
web/models/common.ts
View file @
d3f8ea2d
...
@@ -182,3 +182,17 @@ export type DocumentsLimitResponse = {
...
@@ -182,3 +182,17 @@ export type DocumentsLimitResponse = {
documents_count
:
number
documents_count
:
number
documents_limit
:
number
documents_limit
:
number
}
}
export
type
InvitationResult
=
{
status
:
'success'
email
:
string
url
:
string
}
|
{
status
:
'failed'
email
:
string
message
:
string
}
export
type
InvitationResponse
=
CommonResponse
&
{
invitation_results
:
InvitationResult
[]
}
web/package.json
View file @
d3f8ea2d
...
@@ -54,6 +54,7 @@
...
@@ -54,6 +54,7 @@
"react-i18next"
:
"^12.2.0"
,
"react-i18next"
:
"^12.2.0"
,
"react-infinite-scroll-component"
:
"^6.1.0"
,
"react-infinite-scroll-component"
:
"^6.1.0"
,
"react-markdown"
:
"^8.0.6"
,
"react-markdown"
:
"^8.0.6"
,
"react-multi-email"
:
"^1.0.14"
,
"react-papaparse"
:
"^4.1.0"
,
"react-papaparse"
:
"^4.1.0"
,
"react-slider"
:
"^2.0.4"
,
"react-slider"
:
"^2.0.4"
,
"react-sortablejs"
:
"^6.1.4"
,
"react-sortablejs"
:
"^6.1.4"
,
...
...
web/service/common.ts
View file @
d3f8ea2d
...
@@ -5,7 +5,7 @@ import type {
...
@@ -5,7 +5,7 @@ import type {
DocumentsLimitResponse
,
DocumentsLimitResponse
,
FileUploadConfigResponse
,
FileUploadConfigResponse
,
ICurrentWorkspace
,
ICurrentWorkspace
,
IWorkspace
,
LangGeniusVersionResponse
,
Member
,
IWorkspace
,
InvitationResponse
,
LangGeniusVersionResponse
,
Member
,
OauthResponse
,
PluginProvider
,
Provider
,
ProviderAnthropicToken
,
ProviderAzureToken
,
OauthResponse
,
PluginProvider
,
Provider
,
ProviderAnthropicToken
,
ProviderAzureToken
,
SetupStatusResponse
,
UserProfileOriginResponse
,
SetupStatusResponse
,
UserProfileOriginResponse
,
}
from
'@/models/common'
}
from
'@/models/common'
...
@@ -70,8 +70,8 @@ export const fetchAccountIntegrates: Fetcher<{ data: AccountIntegrate[] | null }
...
@@ -70,8 +70,8 @@ export const fetchAccountIntegrates: Fetcher<{ data: AccountIntegrate[] | null }
return
get
(
url
,
{
params
})
as
Promise
<
{
data
:
AccountIntegrate
[]
|
null
}
>
return
get
(
url
,
{
params
})
as
Promise
<
{
data
:
AccountIntegrate
[]
|
null
}
>
}
}
export
const
inviteMember
:
Fetcher
<
CommonResponse
&
{
account
:
Member
;
invite_url
:
string
}
,
{
url
:
string
;
body
:
Record
<
string
,
any
>
}
>
=
({
url
,
body
})
=>
{
export
const
inviteMember
:
Fetcher
<
InvitationResponse
,
{
url
:
string
;
body
:
Record
<
string
,
any
>
}
>
=
({
url
,
body
})
=>
{
return
post
(
url
,
{
body
})
as
Promise
<
CommonResponse
&
{
account
:
Member
;
invite_url
:
string
}
>
return
post
(
url
,
{
body
})
as
Promise
<
InvitationResponse
>
}
}
export
const
updateMemberRole
:
Fetcher
<
CommonResponse
,
{
url
:
string
;
body
:
Record
<
string
,
any
>
}
>
=
({
url
,
body
})
=>
{
export
const
updateMemberRole
:
Fetcher
<
CommonResponse
,
{
url
:
string
;
body
:
Record
<
string
,
any
>
}
>
=
({
url
,
body
})
=>
{
...
...
web/yarn.lock
View file @
d3f8ea2d
...
@@ -4618,6 +4618,11 @@ react-markdown@^8.0.6:
...
@@ -4618,6 +4618,11 @@ react-markdown@^8.0.6:
unist-util-visit "^4.0.0"
unist-util-visit "^4.0.0"
vfile "^5.0.0"
vfile "^5.0.0"
react-multi-email@^1.0.14:
version "1.0.16"
resolved "https://registry.yarnpkg.com/react-multi-email/-/react-multi-email-1.0.16.tgz#126f78011d02cc27d7462f47befbafff2a53d683"
integrity sha512-dgg4TY3P5FWz6c4ghgxH1bjZOgYL3S/HN+EUNe6dqHbLMVzeyud1ztDUlqvft4NX1sUxKx2IF2zDq1yAJQA5yQ==
react-papaparse@^4.1.0:
react-papaparse@^4.1.0:
version "4.1.0"
version "4.1.0"
resolved "https://registry.yarnpkg.com/react-papaparse/-/react-papaparse-4.1.0.tgz#09e1cdae55a0f3e36650aaf7ff9bb5ee2aed164a"
resolved "https://registry.yarnpkg.com/react-papaparse/-/react-papaparse-4.1.0.tgz#09e1cdae55a0f3e36650aaf7ff9bb5ee2aed164a"
...
...
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