feat: knowledge admin role (#5965)

Co-authored-by: JzoNg <jzongcode@gmail.com>
Co-authored-by: jyong <718720800@qq.com>
This commit is contained in:
Joe
2024-07-04 16:21:40 +08:00
committed by GitHub
parent 46eca01fa3
commit 5d9ad430af
46 changed files with 1028 additions and 350 deletions

View File

@@ -35,6 +35,7 @@ import CustomPage from '@/app/components/custom/custom-page'
import Modal from '@/app/components/base/modal'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import { useProviderContext } from '@/context/provider-context'
import { useAppContext } from '@/context/app-context'
const iconClassName = `
w-4 h-4 ml-3 mr-2
@@ -64,8 +65,11 @@ export default function AccountSetting({
const [activeMenu, setActiveMenu] = useState(activeTab)
const { t } = useTranslation()
const { enableBilling, enableReplaceWebAppLogo } = useProviderContext()
const { isCurrentWorkspaceDatasetOperator } = useAppContext()
const workplaceGroupItems = (() => {
if (isCurrentWorkspaceDatasetOperator)
return []
return [
{
key: 'provider',
@@ -172,7 +176,9 @@ export default function AccountSetting({
{
menuItems.map(menuItem => (
<div key={menuItem.key} className='mb-4'>
<div className='px-2 mb-[6px] text-[10px] sm:text-xs font-medium text-gray-500'>{menuItem.name}</div>
{!isCurrentWorkspaceDatasetOperator && (
<div className='px-2 mb-[6px] text-[10px] sm:text-xs font-medium text-gray-500'>{menuItem.name}</div>
)}
<div>
{
menuItem.items.map(item => (

View File

@@ -29,6 +29,7 @@ const MembersPage = () => {
owner: t('common.members.owner'),
admin: t('common.members.admin'),
editor: t('common.members.editor'),
dataset_operator: t('common.members.datasetOperator'),
normal: t('common.members.normal'),
}
const { locale } = useContext(I18n)

View File

@@ -1,13 +1,12 @@
'use client'
import { Fragment, useCallback, useMemo, useState } from 'react'
import { useCallback, useState } from 'react'
import { useContext } from 'use-context-selector'
import { XMarkIcon } from '@heroicons/react/24/outline'
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 RoleSelector from './role-selector'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import { inviteMember } from '@/service/common'
@@ -31,29 +30,14 @@ const InviteModal = ({
const { notify } = useContext(ToastContext)
const { locale } = useContext(I18n)
const InvitingRoles = useMemo(() => [
{
name: 'normal',
description: t('common.members.normalTip'),
},
{
name: 'editor',
description: t('common.members.editorTip'),
},
{
name: 'admin',
description: t('common.members.adminTip'),
},
], [t])
const [role, setRole] = useState(InvitingRoles[0])
const [role, setRole] = useState<string>('normal')
const handleSend = useCallback(async () => {
if (emails.map((email: string) => emailRegex.test(email)).every(Boolean)) {
try {
const { result, invitation_results } = await inviteMember({
url: '/workspaces/current/members/invite-email',
body: { emails, role: role.name, language: locale },
body: { emails, role, language: locale },
})
if (result === 'success') {
@@ -99,53 +83,9 @@ const InviteModal = ({
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 mr-2',
'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 text-gray-500`}>
{role.description}
</span>
</div>
</div>
)}
</Listbox.Option>,
)}
</Listbox.Options>
</Transition>
</div>
</Listbox>
<div className='mb-6'>
<RoleSelector value={role} onChange={setRole} />
</div>
<Button
tabIndex={0}
className='w-full'

View File

@@ -0,0 +1,95 @@
import { useTranslation } from 'react-i18next'
import cn from 'classnames'
import React, { useState } from 'react'
import { RiArrowDownSLine } from '@remixicon/react'
import { useProviderContext } from '@/context/provider-context'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import { Check } from '@/app/components/base/icons/src/vender/line/general'
export type RoleSelectorProps = {
value: string
onChange: (role: string) => void
}
const RoleSelector = ({ value, onChange }: RoleSelectorProps) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const { datasetOperatorEnabled } = useProviderContext()
const toHump = (name: string) => name.replace(/_(\w)/g, (all, letter) => letter.toUpperCase())
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-start'
offset={4}
>
<div className='relative'>
<PortalToFollowElemTrigger
onClick={() => setOpen(v => !v)}
className='block'
>
<div className={cn('flex items-center px-3 py-2 rounded-lg bg-gray-100 cursor-pointer hover:bg-gray-200', open && 'bg-gray-200')}>
<div className='grow mr-2 text-gray-900 text-sm leading-5'>{t('common.members.invitedAsRole', { role: t(`common.members.${toHump(value)}`) })}</div>
<RiArrowDownSLine className='shrink-0 w-4 h-4 text-gray-700' />
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1002]'>
<div className='relative w-[336px] bg-white rounded-lg border-[0.5px] bg-gray-200 shadow-lg'>
<div className='p-1'>
<div className='p-2 rounded-lg hover:bg-gray-50 cursor-pointer' onClick={() => {
onChange('normal')
setOpen(false)
}}>
<div className='relative pl-5'>
<div className='text-gray-700 text-sm leading-5'>{t('common.members.normal')}</div>
<div className='text-gray-500 text-xs leading-[18px]'>{t('common.members.normalTip')}</div>
{value === 'normal' && <Check className='absolute top-0.5 left-0 w-4 h-4 text-primary-600'/>}
</div>
</div>
<div className='p-2 rounded-lg hover:bg-gray-50 cursor-pointer' onClick={() => {
onChange('editor')
setOpen(false)
}}>
<div className='relative pl-5'>
<div className='text-gray-700 text-sm leading-5'>{t('common.members.editor')}</div>
<div className='text-gray-500 text-xs leading-[18px]'>{t('common.members.editorTip')}</div>
{value === 'editor' && <Check className='absolute top-0.5 left-0 w-4 h-4 text-primary-600'/>}
</div>
</div>
<div className='p-2 rounded-lg hover:bg-gray-50 cursor-pointer' onClick={() => {
onChange('admin')
setOpen(false)
}}>
<div className='relative pl-5'>
<div className='text-gray-700 text-sm leading-5'>{t('common.members.admin')}</div>
<div className='text-gray-500 text-xs leading-[18px]'>{t('common.members.adminTip')}</div>
{value === 'admin' && <Check className='absolute top-0.5 left-0 w-4 h-4 text-primary-600'/>}
</div>
</div>
{datasetOperatorEnabled && (
<div className='p-2 rounded-lg hover:bg-gray-50 cursor-pointer' onClick={() => {
onChange('dataset_operator')
setOpen(false)
}}>
<div className='relative pl-5'>
<div className='text-gray-700 text-sm leading-5'>{t('common.members.datasetOperator')}</div>
<div className='text-gray-500 text-xs leading-[18px]'>{t('common.members.datasetOperatorTip')}</div>
{value === 'dataset_operator' && <Check className='absolute top-0.5 left-0 w-4 h-4 text-primary-600'/>}
</div>
</div>
)}
</div>
</div>
</PortalToFollowElemContent>
</div>
</PortalToFollowElem>
)
}
export default RoleSelector

View File

@@ -1,11 +1,12 @@
'use client'
import { useTranslation } from 'react-i18next'
import { Fragment } from 'react'
import { Fragment, useMemo } from 'react'
import { useContext } from 'use-context-selector'
import { Menu, Transition } from '@headlessui/react'
import cn from 'classnames'
import { CheckIcon, ChevronDownIcon } from '@heroicons/react/24/outline'
import s from './index.module.css'
import { useProviderContext } from '@/context/provider-context'
import type { Member } from '@/models/common'
import { deleteMemberOrCancelInvitation, updateMemberRole } from '@/service/common'
import { ToastContext } from '@/app/components/base/toast'
@@ -33,13 +34,22 @@ const Operation = ({
onOperate,
}: IOperationProps) => {
const { t } = useTranslation()
const { datasetOperatorEnabled } = useProviderContext()
const RoleMap = {
owner: t('common.members.owner'),
admin: t('common.members.admin'),
editor: t('common.members.editor'),
normal: t('common.members.normal'),
dataset_operator: t('common.members.datasetOperator'),
}
const roleList = useMemo(() => {
return [
...['admin', 'editor', 'normal'],
...(datasetOperatorEnabled ? ['dataset_operator'] : []),
]
}, [datasetOperatorEnabled])
const { notify } = useContext(ToastContext)
const toHump = (name: string) => name.replace(/_(\w)/g, (all, letter) => letter.toUpperCase())
const handleDeleteMemberOrCancelInvitation = async () => {
try {
await deleteMemberOrCancelInvitation({ url: `/workspaces/current/members/${member.id}` })
@@ -99,7 +109,7 @@ const Operation = ({
>
<div className="px-1 py-1">
{
['admin', 'editor', 'normal'].map(role => (
roleList.map(role => (
<Menu.Item key={role}>
<div className={itemClassName} onClick={() => handleUpdateMemberRole(role)}>
{
@@ -108,8 +118,8 @@ const Operation = ({
: <div className={itemIconClassName} />
}
<div>
<div className={itemTitleClassName}>{t(`common.members.${role}`)}</div>
<div className={itemDescClassName}>{t(`common.members.${role}Tip`)}</div>
<div className={itemTitleClassName}>{t(`common.members.${toHump(role)}`)}</div>
<div className={itemDescClassName}>{t(`common.members.${toHump(role)}Tip`)}</div>
</div>
</div>
</Menu.Item>

View File

@@ -26,7 +26,7 @@ const navClassName = `
`
const Header = () => {
const { isCurrentWorkspaceEditor } = useAppContext()
const { isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator } = useAppContext()
const selectedSegment = useSelectedLayoutSegment()
const media = useBreakpoints()
@@ -72,10 +72,10 @@ const Header = () => {
)}
{!isMobile && (
<div className='flex items-center'>
<ExploreNav className={navClassName} />
<AppNav />
{isCurrentWorkspaceEditor && <DatasetNav />}
<ToolsNav className={navClassName} />
{!isCurrentWorkspaceDatasetOperator && <ExploreNav className={navClassName} />}
{!isCurrentWorkspaceDatasetOperator && <AppNav />}
{(isCurrentWorkspaceEditor || isCurrentWorkspaceDatasetOperator) && <DatasetNav />}
{!isCurrentWorkspaceDatasetOperator && <ToolsNav className={navClassName} />}
</div>
)}
<div className='flex items-center flex-shrink-0'>
@@ -91,10 +91,10 @@ const Header = () => {
</div>
{(isMobile && isShowNavMenu) && (
<div className='w-full flex flex-col p-2 gap-y-1'>
<ExploreNav className={navClassName} />
<AppNav />
{isCurrentWorkspaceEditor && <DatasetNav />}
<ToolsNav className={navClassName} />
{!isCurrentWorkspaceDatasetOperator && <ExploreNav className={navClassName} />}
{!isCurrentWorkspaceDatasetOperator && <AppNav />}
{(isCurrentWorkspaceEditor || isCurrentWorkspaceDatasetOperator) && <DatasetNav />}
{!isCurrentWorkspaceDatasetOperator && <ToolsNav className={navClassName} />}
</div>
)}
</div>

View File

@@ -113,7 +113,7 @@ const NavSelector = ({ curNav, navs, createText, isApp, onCreate, onLoadmore }:
))
}
</div>
{!isApp && (
{!isApp && isCurrentWorkspaceEditor && (
<Menu.Button className='p-1 w-full'>
<div onClick={() => onCreate('')} className={cn(
'flex items-center gap-2 px-3 py-[6px] rounded-lg cursor-pointer hover:bg-gray-100',