Initial commit
This commit is contained in:
commit
db896255d6
744 changed files with 56028 additions and 0 deletions
|
|
@ -0,0 +1,9 @@
|
|||
.logo-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: #ffffff url(../../assets/logo-icon.png) center center no-repeat;
|
||||
background-size: contain;
|
||||
box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05);
|
||||
border-radius: 8px;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
114
web/app/components/header/account-setting/members-page/index.tsx
Normal file
114
web/app/components/header/account-setting/members-page/index.tsx
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
'use client'
|
||||
import { useState } from 'react'
|
||||
import s from './index.module.css'
|
||||
import cn from 'classnames'
|
||||
import useSWR from 'swr'
|
||||
import dayjs from 'dayjs'
|
||||
import 'dayjs/locale/zh-cn'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import I18n from '@/context/i18n'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { fetchMembers } from '@/service/common'
|
||||
import { UserPlusIcon } from '@heroicons/react/24/outline'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import InviteModal from './invite-modal'
|
||||
import InvitedModal from './invited-modal'
|
||||
import Operation from './operation'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import Avatar from '@/app/components/base/avatar'
|
||||
import { useWorkspacesContext } from '@/context/workspace-context'
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
const MembersPage = () => {
|
||||
const { t } = useTranslation()
|
||||
const RoleMap = {
|
||||
owner: t('common.members.owner'),
|
||||
admin: t('common.members.admin'),
|
||||
normal: t('common.members.normal'),
|
||||
}
|
||||
const { locale } = useContext(I18n)
|
||||
const { userProfile } = useAppContext()
|
||||
const { data, mutate } = useSWR({ url: '/workspaces/current/members' }, fetchMembers)
|
||||
const [inviteModalVisible, setInviteModalVisible] = useState(false)
|
||||
const [invitedModalVisible, setInvitedModalVisible] = useState(false)
|
||||
const accounts = data?.accounts || []
|
||||
const owner = accounts.filter(account => account.role === 'owner')?.[0]?.email === userProfile.email
|
||||
const { workspaces } = useWorkspacesContext()
|
||||
const currentWrokspace = workspaces.filter(item => item.current)?.[0]
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div className='flex items-center mb-4 p-3 bg-gray-50 rounded-2xl'>
|
||||
<div className={cn(s['logo-icon'], 'shrink-0')}></div>
|
||||
<div className='grow mx-2'>
|
||||
<div className='text-sm font-medium text-gray-900'>{currentWrokspace.name}</div>
|
||||
<div className='text-xs text-gray-500'>{t('common.userProfile.workspace')}</div>
|
||||
</div>
|
||||
<div className='
|
||||
shrink-0 flex items-center py-[7px] px-3 border-[0.5px] border-gray-200
|
||||
text-[13px] font-medium text-primary-600 bg-white
|
||||
shadow-[0_1px_2px_rgba(16,24,40,0.05)] rounded-lg cursor-pointer
|
||||
' onClick={() => setInviteModalVisible(true)}>
|
||||
<UserPlusIcon className='w-4 h-4 mr-2 ' />
|
||||
{t('common.members.invite')}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className='flex items-center py-[7px] border-b border-gray-200'>
|
||||
<div className='grow px-3 text-xs font-medium text-gray-500'>{t('common.members.name')}</div>
|
||||
<div className='shrink-0 w-[104px] text-xs font-medium text-gray-500'>{t('common.members.lastActive')}</div>
|
||||
<div className='shrink-0 w-[96px] px-3 text-xs font-medium text-gray-500'>{t('common.members.role')}</div>
|
||||
</div>
|
||||
<div>
|
||||
{
|
||||
accounts.map(account => (
|
||||
<div key={account.id} className='flex border-b border-gray-100'>
|
||||
<div className='grow flex items-center py-2 px-3'>
|
||||
<Avatar size={24} className='mr-2' name={account.name} />
|
||||
<div className=''>
|
||||
<div className='text-[13px] font-medium text-gray-700 leading-[18px]'>
|
||||
{account.name}
|
||||
{account.status === 'pending' && <span className='ml-1 text-xs text-[#DC6803]'>{t('common.members.pending')}</span>}
|
||||
{userProfile.email === account.email && <span className='text-xs text-gray-500'>{t('common.members.you')}</span>}
|
||||
</div>
|
||||
<div className='text-xs text-gray-500 leading-[18px]'>{account.email}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='shrink-0 flex items-center w-[104px] py-2 text-[13px] text-gray-700'>{dayjs(Number((account.last_login_at || account.created_at)) * 1000).locale(locale === 'zh-Hans' ? 'zh-cn' : 'en').fromNow()}</div>
|
||||
<div className='shrink-0 w-[96px] flex items-center'>
|
||||
{
|
||||
owner && account.role !== 'owner'
|
||||
? <Operation member={account} onOperate={() => mutate()} />
|
||||
: <div className='px-3 text-[13px] text-gray-700'>{RoleMap[account.role] || RoleMap.normal}</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
inviteModalVisible && (
|
||||
<InviteModal
|
||||
onCancel={() => setInviteModalVisible(false)}
|
||||
onSend={() => {
|
||||
setInvitedModalVisible(true)
|
||||
mutate()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
invitedModalVisible && (
|
||||
<InvitedModal
|
||||
onCancel={() => setInvitedModalVisible(false)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default MembersPage
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
.modal {
|
||||
padding: 24px 32px !important;
|
||||
width: 400px !important;
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
'use client'
|
||||
import { useState } from 'react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { XMarkIcon } from '@heroicons/react/24/outline'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import s from './index.module.css'
|
||||
import { inviteMember } from '@/service/common'
|
||||
import { emailRegex } from '@/config'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
|
||||
interface IInviteModalProps {
|
||||
onCancel: () => void,
|
||||
onSend: () => void,
|
||||
}
|
||||
const InviteModal = ({
|
||||
onCancel,
|
||||
onSend,
|
||||
}: IInviteModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [email, setEmail] = useState('')
|
||||
const { notify } = useContext(ToastContext)
|
||||
|
||||
const handleSend = async () => {
|
||||
if (emailRegex.test(email)) {
|
||||
try {
|
||||
const res = await inviteMember({ url: '/workspaces/current/members/invite-email', body: { email, role: 'admin'} })
|
||||
|
||||
if (res.result === 'success') {
|
||||
onCancel()
|
||||
onSend()
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
} else {
|
||||
notify({ type: 'error', message: t('common.members.emailInvalid') })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={s.wrap}>
|
||||
<Modal isShow onClose={() => {}} className={s.modal}>
|
||||
<div className='flex justify-between mb-2'>
|
||||
<div className='text-xl font-semibold text-gray-900'>{t('common.members.inviteTeamMember')}</div>
|
||||
<XMarkIcon className='w-4 h-4 cursor-pointer' onClick={onCancel} />
|
||||
</div>
|
||||
<div className='mb-7 text-[13px] text-gray-500'>{t('common.members.inviteTeamMemberTip')}</div>
|
||||
<div>
|
||||
<div className='mb-2 text-sm font-medium text-gray-900'>{t('common.members.email')}</div>
|
||||
<input
|
||||
className='
|
||||
block w-full py-2 mb-9 px-3 bg-gray-50 outline-none border-none
|
||||
appearance-none text-sm text-gray-900 rounded-lg
|
||||
'
|
||||
value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
placeholder={t('common.members.emailPlaceholder') || ''}
|
||||
/>
|
||||
<Button
|
||||
className='w-full text-sm font-medium'
|
||||
onClick={handleSend}
|
||||
type='primary'
|
||||
>
|
||||
{t('common.members.sendInvite')}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default InviteModal
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
.modal {
|
||||
padding: 32px !important;
|
||||
width: 480px !important;
|
||||
background: linear-gradient(180deg, rgba(3, 152, 85, 0.05) 0%, rgba(3, 152, 85, 0) 22.44%), #F9FAFB !important;
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import { CheckCircleIcon } from '@heroicons/react/24/solid'
|
||||
import { XMarkIcon } from '@heroicons/react/24/outline'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import s from './index.module.css'
|
||||
|
||||
interface IInvitedModalProps {
|
||||
onCancel: () => void,
|
||||
}
|
||||
const InvitedModal = ({
|
||||
onCancel
|
||||
}: IInvitedModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className={s.wrap}>
|
||||
<Modal isShow onClose={() => {}} className={s.modal}>
|
||||
<div className='flex justify-between mb-3'>
|
||||
<div className='
|
||||
w-12 h-12 flex items-center justify-center rounded-xl
|
||||
bg-white border-[0.5px] border-gray-100
|
||||
shadow-[0px_20px_24px_-4px_rgba(16,24,40,0.08),0px_8px_8px_-4px_rgba(16,24,40,0.03)]
|
||||
'>
|
||||
<CheckCircleIcon className='w-[22px] h-[22px] text-[#039855]' />
|
||||
</div>
|
||||
<XMarkIcon className='w-4 h-4 cursor-pointer' onClick={onCancel} />
|
||||
</div>
|
||||
<div className='mb-1 text-xl font-semibold text-gray-900'>{t('common.members.invitationSent')}</div>
|
||||
<div className='mb-10 text-sm text-gray-500'>{t('common.members.invitationSentTip')}</div>
|
||||
<div className='flex justify-end'>
|
||||
<Button
|
||||
className='w-[96px] text-sm font-medium'
|
||||
onClick={onCancel}
|
||||
type='primary'
|
||||
>
|
||||
{t('common.members.ok')}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default InvitedModal
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
.popup {
|
||||
box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03);
|
||||
}
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
'use client'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Fragment } from 'react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { Menu, Transition } from '@headlessui/react'
|
||||
import type { Member } from '@/models/common'
|
||||
import { deleteMemberOrCancelInvitation, updateMemberRole } from '@/service/common'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import s from './index.module.css'
|
||||
import cn from 'classnames'
|
||||
import { ChevronDownIcon, CheckIcon } from '@heroicons/react/24/outline'
|
||||
|
||||
const itemClassName = `
|
||||
flex px-3 py-2 cursor-pointer hover:bg-gray-50 rounded-lg
|
||||
`
|
||||
const itemIconClassName = `
|
||||
w-4 h-4 mt-[2px] mr-1 text-primary-600
|
||||
`
|
||||
const itemTitleClassName = `
|
||||
leading-[20px] text-sm text-gray-700 whitespace-nowrap
|
||||
`
|
||||
const itemDescClassName = `
|
||||
leading-[18px] text-xs text-gray-500 whitespace-nowrap
|
||||
`
|
||||
|
||||
interface IOperationProps {
|
||||
member: Member
|
||||
onOperate: () => void
|
||||
}
|
||||
|
||||
const Operation = ({
|
||||
member,
|
||||
onOperate
|
||||
}: IOperationProps) => {
|
||||
const { t } = useTranslation()
|
||||
const RoleMap = {
|
||||
owner: t('common.members.owner'),
|
||||
admin: t('common.members.admin'),
|
||||
normal: t('common.members.normal'),
|
||||
}
|
||||
const { notify } = useContext(ToastContext)
|
||||
const handleDeleteMemberOrCancelInvitation = async () => {
|
||||
try {
|
||||
await deleteMemberOrCancelInvitation({ url: `/workspaces/current/members/${member.id}` })
|
||||
onOperate()
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
}
|
||||
const handleUpdateMemberRole = async (role: string) => {
|
||||
try {
|
||||
await updateMemberRole({
|
||||
url: `/workspaces/current/members/${member.id}/update-role`,
|
||||
body: { role }
|
||||
})
|
||||
onOperate()
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu as="div" className="relative w-full h-full">
|
||||
{
|
||||
({ open }) => (
|
||||
<>
|
||||
<Menu.Button className={cn(
|
||||
`
|
||||
group flex items-center justify-between w-full h-full justify-center
|
||||
hover:bg-gray-100 cursor-pointer ${open && 'bg-gray-100'}
|
||||
text-[13px] text-gray-700 px-3
|
||||
`
|
||||
)}>
|
||||
{RoleMap[member.role] || RoleMap.normal}
|
||||
<ChevronDownIcon className={`w-4 h-4 group-hover:block ${open ? 'block' : 'hidden'}`} />
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items
|
||||
className={cn(
|
||||
`
|
||||
absolute right-0 top-[52px] z-10 bg-white border-[0.5px] border-gray-200
|
||||
divide-y divide-gray-100 origin-top-right rounded-lg
|
||||
`,
|
||||
s.popup
|
||||
)}
|
||||
>
|
||||
<div className="px-1 py-1">
|
||||
{
|
||||
['admin', 'normal'].map(role => (
|
||||
<Menu.Item>
|
||||
<div className={itemClassName} onClick={() => handleUpdateMemberRole(role)}>
|
||||
{
|
||||
role === member.role
|
||||
? <CheckIcon className={itemIconClassName} />
|
||||
: <div className={itemIconClassName} />
|
||||
}
|
||||
<div>
|
||||
<div className={itemTitleClassName}>{t(`common.members.${role}`)}</div>
|
||||
<div className={itemDescClassName}>{t(`common.members.${role}Tip`)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<Menu.Item>
|
||||
<div className='px-1 py-1'>
|
||||
<div className={itemClassName} onClick={handleDeleteMemberOrCancelInvitation}>
|
||||
<div className={itemIconClassName} />
|
||||
<div>
|
||||
<div className={itemTitleClassName}>{t('common.members.removeFromTeam')}</div>
|
||||
<div className={itemDescClassName}>{t('common.members.removeFromTeamTip')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
|
||||
export default Operation
|
||||
Loading…
Add table
Add a link
Reference in a new issue