Bladeren bron

Merge branch '1.1.3-master_web' of http://8.130.72.63:18081/shenzhen/tjdify into 1.1.3-master

CzRger 2 maanden geleden
bovenliggende
commit
76905918c3

+ 37 - 26
web/app/(commonLayout)/datasets/Container.tsx

@@ -16,10 +16,7 @@ import Doc from './Doc'
 import TabSliderNew from '@/app/components/base/tab-slider-new'
 import TagManagementModal from '@/app/components/base/tag-management'
 import TagFilter from '@/app/components/base/tag-management/filter'
-import Button from '@/app/components/base/button'
 import Input from '@/app/components/base/input'
-import { ApiConnectionMod } from '@/app/components/base/icons/src/vender/solid/development'
-import CheckboxWithLabel from '@/app/components/datasets/create/website/base/checkbox-with-label'
 
 // Services
 import { fetchDatasetApiBaseUrl } from '@/service/datasets'
@@ -108,15 +105,18 @@ const Container = () => {
   const [optionsDept, setOptionsDept] = useState<any>([])
   useEffect(() => {
     fetchDepts({
-      url: '/xxx',
-      params: {
-        page: 1,
-        limit: 99999,
-      },
+      url: '/depts',
     }).then((res: any) => {
       setOptionsDept(res.data || [])
     })
   }, [])
+  const [authType, setAuthType] = useState<any>('')
+  const optionsAuthType = [
+    { name: '创建', value: '1' },
+    { name: '编辑', value: '2' },
+    { name: '授权编辑', value: '3' },
+    { name: '授权可见', value: '4' },
+  ]
   return (
     <div ref={containerRef} className='scroll-container relative flex grow flex-col overflow-y-auto bg-background-body'>
       <div className='sticky top-0 z-10 flex flex-wrap justify-between gap-y-2 bg-background-body px-12 pb-2 pt-4 leading-[56px]'>
@@ -127,6 +127,16 @@ const Container = () => {
         />
         {activeTab === 'dataset' && (
           <div className='flex items-center justify-center gap-2'>
+            <SimpleSelect
+              wrapperClassName="h-[32px] w-[200px]"
+              defaultValue={authType}
+              onSelect={(i) => {
+                setAuthType(i.value)
+              }}
+              items={optionsAuthType}
+              allowSearch={false}
+              placeholder="请选择权限类型"
+            />
             <AntdTreeSelect
               showSearch
               style={{ width: '200px' }}
@@ -137,7 +147,7 @@ const Container = () => {
               treeDefaultExpandAll
               onChange={v => setDept(v || '')}
               treeData={optionsDept}
-              fieldNames={{ label: 'name', value: 'id' }}
+              fieldNames={{ label: 'dept_name', value: 'dept_id' }}
             />
             <SimpleSelect
               wrapperClassName="h-[32px] w-[200px]"
@@ -148,15 +158,16 @@ const Container = () => {
               }}
               items={optionsType}
               allowSearch={false}
+              placeholder="请选择类型"
             />
-            {isCurrentWorkspaceOwner && <CheckboxWithLabel
-              isChecked={includeAll}
-              onChange={toggleIncludeAll}
-              label={t('dataset.allKnowledge')}
-              labelClassName='system-md-regular text-text-secondary'
-              className='mr-2'
-              tooltip={t('dataset.allKnowledgeDescription') as string}
-            />}
+            {/* {isCurrentWorkspaceOwner && <CheckboxWithLabel */}
+            {/*  isChecked={includeAll} */}
+            {/*  onChange={toggleIncludeAll} */}
+            {/*  label={t('dataset.allKnowledge')} */}
+            {/*  labelClassName='system-md-regular text-text-secondary' */}
+            {/*  className='mr-2' */}
+            {/*  tooltip={t('dataset.allKnowledgeDescription') as string} */}
+            {/* />} */}
             <TagFilter type='knowledge' value={tagFilterValue} onChange={handleTagsChange} />
             <Input
               showLeftIcon
@@ -166,21 +177,21 @@ const Container = () => {
               onChange={e => handleKeywordsChange(e.target.value)}
               onClear={() => handleKeywordsChange('')}
             />
-            <div className="h-4 w-[1px] bg-divider-regular" />
-            <Button
-              className='shadows-shadow-xs gap-0.5'
-              onClick={() => setShowExternalApiPanel(true)}
-            >
-              <ApiConnectionMod className='h-4 w-4 text-components-button-secondary-text' />
-              <div className='system-sm-medium flex items-center justify-center gap-1 px-0.5 text-components-button-secondary-text'>{t('dataset.externalAPIPanelTitle')}</div>
-            </Button>
+            {/* <div className="h-4 w-[1px] bg-divider-regular" /> */}
+            {/* <Button */}
+            {/*  className='shadows-shadow-xs gap-0.5' */}
+            {/*  onClick={() => setShowExternalApiPanel(true)} */}
+            {/* > */}
+            {/*  <ApiConnectionMod className='h-4 w-4 text-components-button-secondary-text' /> */}
+            {/*  <div className='system-sm-medium flex items-center justify-center gap-1 px-0.5 text-components-button-secondary-text'>{t('dataset.externalAPIPanelTitle')}</div> */}
+            {/* </Button> */}
           </div>
         )}
         {activeTab === 'api' && data && <ApiServer apiBaseUrl={data.api_base_url || ''} />}
       </div>
       {activeTab === 'dataset' && (
         <>
-          <Datasets containerRef={containerRef} tags={tagIDs} keywords={searchKeywords} includeAll={includeAll} type={searchType} dept={dept} />
+          <Datasets containerRef={containerRef} tags={tagIDs} keywords={searchKeywords} includeAll={includeAll} type={searchType} dept={dept} authType={authType} />
           <DatasetFooter />
           {showTagManagementModal && (
             <TagManagementModal type='knowledge' show={showTagManagementModal} />

+ 34 - 22
web/app/(commonLayout)/datasets/DatasetCard.tsx

@@ -35,7 +35,7 @@ const DatasetCard = ({
   const { push } = useRouter()
   const EXTERNAL_PROVIDER = 'external' as const
 
-  const { isCurrentWorkspaceDatasetOperator } = useAppContext()
+  const { isCurrentWorkspaceDatasetOperator, currentWorkspace, userProfile } = useAppContext()
   const [tags, setTags] = useState<Tag[]>(dataset.tags)
 
   const [showRenameModal, setShowRenameModal] = useState(false)
@@ -193,28 +193,40 @@ const DatasetCard = ({
               />
             </div>
           </div>
-          <div className='mx-1 !hidden h-[14px] w-[1px] shrink-0 bg-divider-regular group-hover:!flex' />
-          <div className='!hidden shrink-0 group-hover:!flex'>
-            <CustomPopover
-              htmlContent={<Operations showDelete={!isCurrentWorkspaceDatasetOperator} />}
-              position="br"
-              trigger="click"
-              btnElement={
-                <div
-                  className='flex h-8 w-8 cursor-pointer items-center justify-center rounded-md'
-                >
-                  <RiMoreFill className='h-4 w-4 text-text-secondary' />
+          {
+            (
+              currentWorkspace.role === 'owner' // 所有者
+              || currentWorkspace.role === 'admin' // 管理员
+              || dataset.has_edit_permission // 编辑授权
+              || dataset.created_by === userProfile.id // 创建
+              || (dataset.edit_auth === 2 && dataset.dept_id === userProfile.dept_id) // 部门领导
+            ) && (
+              <>
+                <div className='mx-1 !hidden h-[14px] w-[1px] shrink-0 bg-divider-regular group-hover:!flex' />
+                <div className='!hidden shrink-0 group-hover:!flex'>
+                  <CustomPopover
+                    htmlContent={<Operations showDelete={!isCurrentWorkspaceDatasetOperator} />}
+                    position="br"
+                    trigger="click"
+                    btnElement={
+                      <div
+                        className='flex h-8 w-8 cursor-pointer items-center justify-center rounded-md'
+                      >
+                        <RiMoreFill className='h-4 w-4 text-text-secondary' />
+                      </div>
+                    }
+                    btnClassName={open =>
+                      cn(
+                        open ? '!bg-black/5 !shadow-none' : '!bg-transparent',
+                        'h-8 w-8 rounded-md border-none !p-2 hover:!bg-black/5',
+                      )
+                    }
+                    className={'!z-20 h-fit !w-[128px]'}
+                  />
                 </div>
-              }
-              btnClassName={open =>
-                cn(
-                  open ? '!bg-black/5 !shadow-none' : '!bg-transparent',
-                  'h-8 w-8 rounded-md border-none !p-2 hover:!bg-black/5',
-                )
-              }
-              className={'!z-20 h-fit !w-[128px]'}
-            />
-          </div>
+              </>
+            )
+          }
         </div>
       </div>
       {showRenameModal && (

+ 7 - 2
web/app/(commonLayout)/datasets/Datasets.tsx

@@ -18,6 +18,7 @@ const getKey = (
   includeAll: boolean,
   type: string,
   dept: string,
+  authType: string,
 ) => {
   if (!pageIndex || previousPageData.has_more) {
     const params: FetchDatasetsParams = {
@@ -35,7 +36,9 @@ const getKey = (
     if (type)
       params.params.category_ids = [type]
     if (dept)
-      params.params.dept = dept
+      params.params.creatorDept = dept
+    if (authType)
+      params.params.authType = authType
     return params
   }
   return null
@@ -48,6 +51,7 @@ type Props = {
   includeAll: boolean,
   type: string,
   dept: string,
+  authType: string,
 }
 
 const Datasets = ({
@@ -57,10 +61,11 @@ const Datasets = ({
   includeAll,
   type,
   dept,
+  authType,
 }: Props) => {
   const { isCurrentWorkspaceEditor } = useAppContext()
   const { data, isLoading, setSize, mutate } = useSWRInfinite(
-    (pageIndex: number, previousPageData: DataSetListResponse) => getKey(pageIndex, previousPageData, tags, keywords, includeAll, type, dept),
+    (pageIndex: number, previousPageData: DataSetListResponse) => getKey(pageIndex, previousPageData, tags, keywords, includeAll, type, dept, authType),
     fetchDatasets,
     { revalidateFirstPage: false, revalidateAll: true },
   )

+ 57 - 21
web/app/components/datasets/rename-modal/index.tsx

@@ -17,7 +17,12 @@ import { ToastContext } from '@/app/components/base/toast'
 import type { DataSet } from '@/models/datasets'
 import { tagBindingsCreate, tagBindingsRemove, updateDatasetSetting } from '@/service/datasets'
 import { useModalContext } from '@/context/modal-context'
-import { fetchDeptUsers, fetchTypes } from '@/service/common'
+import {
+  fetchDatasetsPermission,
+  fetchDeptUserTree,
+  fetchTypes,
+  setDatasetsPermission,
+} from '@/service/common'
 import { TreeSelect as AntdTreeSelect } from 'antd'
 
 type RenameDatasetModalProps = {
@@ -36,6 +41,9 @@ const RenameDatasetModal = ({ show, dataset, onSuccess, onClose }: RenameDataset
   const [externalKnowledgeId, setExternalKnowledgeId] = useState<string>(dataset.external_knowledge_info.external_knowledge_id)
   const [externalKnowledgeApiId, setExternalKnowledgeApiId] = useState<string>(dataset.external_knowledge_info.external_knowledge_api_id)
   const [type, setType] = useState<any>(dataset.categories[0]?.id)
+  const [editAuth, setEditAuth] = useState()
+  const [editUserIds, setEditUserIds] = useState([])
+  const [lookUserIds, setLookUserIds] = useState([])
   const [options, setOptions] = useState<any>([])
   useEffect(() => {
     fetchTypes({
@@ -87,6 +95,14 @@ const RenameDatasetModal = ({ show, dataset, onSuccess, onClose }: RenameDataset
           },
         })
       }
+      await setDatasetsPermission({
+        url: `/datasets/${dataset.id}/permission`,
+        body: {
+          edit_auth: editAuth,
+          edit_permission: editUserIds.map((v: any) => ({ id: v })),
+          read_permission: lookUserIds.map((v: any) => ({ id: v })),
+        },
+      })
       notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
       if (onSuccess)
         onSuccess()
@@ -104,31 +120,51 @@ const RenameDatasetModal = ({ show, dataset, onSuccess, onClose }: RenameDataset
     { name: '本账号', value: 1 },
     { name: '本部门', value: 2 },
   ]
-  const [editAuth, setEditAuth] = useState()
-  const [editUserIds, setEditUserIds] = useState([])
-  const [lookUserIds, setLookUserIds] = useState([])
   const [optionsDeptUser, setOptionsDeptUser] = useState<any>([])
   const [optionsDeptUserEdit, setOptionsDeptUserEdit] = useState<any>([])
   useEffect(() => {
-    fetchDeptUsers({
-      url: '/xxx',
-      params: {
-        page: 1,
-        limit: 99999,
-      },
+    fetchDatasetsPermission({
+      url: `/datasets/${dataset.id}/permission`,
     }).then((res: any) => {
-      setOptionsDeptUser(res.data || [])
+      setEditAuth(res.edit_auth)
+      setEditUserIds(res.edit_permission.map((v: any) => v.id))
+      setLookUserIds(res.read_permission.map((v: any) => v.id))
     })
   }, [])
   useEffect(() => {
-    fetchDeptUsers({
-      url: '/xxx',
-      params: {
-        page: 1,
-        limit: 99999,
-      },
+    fetchDeptUserTree({
+      url: '/dept/dept-accounts',
     }).then((res: any) => {
-      setOptionsDeptUserEdit(res.data || [])
+      const deep = (arr: any) => {
+        return arr.map((v: any) => {
+          v.treeId = v.dept_id || v.account_id
+          v.treeName = v.dept_name || v.email
+          if (v.children?.length > 0)
+            v.treeChildren = deep(v.children)
+          else if (v.accounts?.length > 0)
+            v.treeChildren = deep(v.accounts)
+          return v
+        })
+      }
+      setOptionsDeptUser(deep(res.data) || [])
+    })
+  }, [])
+  useEffect(() => {
+    fetchDeptUserTree({
+      url: '/dept/dept-accounts',
+    }).then((res: any) => {
+      const deep = (arr: any) => {
+        return arr.map((v: any) => {
+          v.treeId = v.dept_id || v.account_id
+          v.treeName = v.dept_name || v.email
+          if (v.children?.length > 0)
+            v.treeChildren = deep(v.children)
+          else if (v.accounts?.length > 0)
+            v.treeChildren = deep(v.accounts)
+          return v
+        })
+      }
+      setOptionsDeptUserEdit(deep(res.data) || [])
     })
   }, [])
   return (
@@ -189,7 +225,7 @@ const RenameDatasetModal = ({ show, dataset, onSuccess, onClose }: RenameDataset
             <SimpleSelect
               className="h-[32px]"
               defaultValue={editAuth}
-              onSelect={(i) => {
+              onSelect={(i: any) => {
                 setEditAuth(i.value)
               }}
               items={optionsEditAuth}
@@ -210,7 +246,7 @@ const RenameDatasetModal = ({ show, dataset, onSuccess, onClose }: RenameDataset
             treeDefaultExpandAll
             onChange={v => setEditUserIds(v)}
             treeData={optionsDeptUserEdit}
-            fieldNames={{ label: 'name', value: 'id' }}
+            fieldNames={{ label: 'treeName', value: 'treeId', children: 'treeChildren' }}
             multiple={true}
             treeCheckable={true}
           />
@@ -227,7 +263,7 @@ const RenameDatasetModal = ({ show, dataset, onSuccess, onClose }: RenameDataset
             treeDefaultExpandAll
             onChange={v => setLookUserIds(v)}
             treeData={optionsDeptUser}
-            fieldNames={{ label: 'name', value: 'id' }}
+            fieldNames={{ label: 'treeName', value: 'treeId', children: 'treeChildren' }}
             multiple={true}
             treeCheckable={true}
           />

+ 21 - 21
web/app/components/header/account-setting/dept-page/detail-modal/index.tsx

@@ -1,11 +1,11 @@
 'use client'
-import { useCallback, useEffect, useState } from 'react'
+import { useEffect, useState } from 'react'
 import { RiCloseLine } from '@remixicon/react'
 import s from './index.module.css'
 import cn from '@/utils/classnames'
 import Modal from '@/app/components/base/modal'
 import Button from '@/app/components/base/button'
-import { addKnowledge, editKnowledge, fetchDepts } from '@/service/common'
+import { addDept, editDept, fetchDepts } from '@/service/common'
 import 'react-multi-email/dist/style.css'
 import Input from '@/app/components/base/input'
 import { TreeSelect as AntdTreeSelect } from 'antd'
@@ -15,42 +15,40 @@ const InviteModal = ({
   onCancel,
   onSend,
 }: any) => {
-  const [name, setName] = useState<string>(transfer.row?.name || '')
+  const [name, setName] = useState<string>(transfer.row?.dept_name || '')
+  const [parent, setParent] = useState<string>(transfer.row?.parent_dept_id || '')
+  console.log(transfer)
   const [options, setOptions] = useState<any>([])
   useEffect(() => {
     fetchDepts({
-      url: '/xxx',
-      params: {
-        page: 1,
-        limit: 99999,
-      },
+      url: '/depts',
     }).then((res: any) => {
       setOptions(res.data || [])
     })
   }, [])
-  const handleSave = useCallback(async () => {
+  const handleSave = async () => {
     try {
       let res: any = () => {}
       if (transfer.mode === 'add') {
-        res = await addKnowledge({
-          url: '/123',
-          body: { name, status: false },
+        res = await addDept({
+          url: '/depts',
+          body: { dept_name: name, parent_dept_id: parent },
         })
       }
       else {
-        res = await editKnowledge({
-          url: `/123/${transfer.row.id}`,
-          body: { name },
+        res = await editDept({
+          url: '/depts',
+          body: { dept_name: name, parent_dept_id: parent, dept_id: transfer.row.dept_id },
         })
       }
-      const { id }: any = res
-      if (id) {
+      const { result }: any = res
+      if (result === 'success') {
         onCancel()
         onSend()
       }
     }
     catch (e) { }
-  }, [name, onCancel, onSend, transfer])
+  }
 
   return (
     <div className={cn(s.wrap)}>
@@ -67,14 +65,16 @@ const InviteModal = ({
             <AntdTreeSelect
               showSearch
               style={{ width: '100%' }}
-              value={name}
+              value={parent}
               dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
               placeholder="请选择上级部门"
               allowClear
               treeDefaultExpandAll
-              onChange={v => setName(v || '')}
+              onChange={(v) => {
+                setParent(v || '')
+              }}
               treeData={options}
-              fieldNames={{ label: 'name', value: 'id' }}
+              fieldNames={{ label: 'dept_name', value: 'dept_id' }}
             />
           </div>
           <div className={cn('flex flex-wrap items-center justify-between')}>

+ 8 - 54
web/app/components/header/account-setting/dept-page/index.tsx

@@ -9,7 +9,7 @@ import UserModal from './user-modal'
 import { useContext } from 'use-context-selector'
 import { RiAddLine } from '@remixicon/react'
 import { useTranslation } from 'react-i18next'
-import { delKnowledge, fetchDepts } from '@/service/common'
+import { delDept, fetchDepts } from '@/service/common'
 import I18n from '@/context/i18n'
 import { useAppContext } from '@/context/app-context'
 import LogoEmbeddedChatHeader from '@/app/components/base/logo/logo-embedded-chat-header'
@@ -32,10 +32,8 @@ const DeptsPage = () => {
   const { userProfile, currentWorkspace, isCurrentWorkspaceOwner, isCurrentWorkspaceManager, systemFeatures } = useAppContext()
   const { data, mutate }: any = useSWR(
     {
-      url: '/external_applications',
+      url: '/depts',
       params: {
-        page: 1,
-        limit: 99999,
       },
     },
     fetchDepts,
@@ -50,13 +48,12 @@ const DeptsPage = () => {
   const { plan, enableBilling } = useProviderContext()
   const isNotUnlimitedMemberPlan = enableBilling && plan.type !== Plan.team && plan.type !== Plan.enterprise
   const isMemberFull = enableBilling && isNotUnlimitedMemberPlan && deptList.length >= plan.total.teamMembers
-
   const [showConfirmDelete, setShowConfirmDelete] = useState(false)
   const [row, setRow] = useState<any>({})
   const handleDel = async () => {
     try {
-      await delKnowledge({
-        url: `/external_applications/${row.id}`,
+      await delDept({
+        url: `/depts/${row.dept_id}`,
         body: {},
       })
       setShowConfirmDelete(false)
@@ -105,12 +102,12 @@ const DeptsPage = () => {
         </div>
         <div className='overflow-visible lg:overflow-visible'>
           <AntdTable
-            rowKey="id"
+            rowKey="dept_id"
             dataSource={deptList}
             pagination={false}
           >
-            <AntdColumn title="部门名称" dataIndex="name" key="name"/>
-            <AntdColumn title="关联用户数量" dataIndex="relation" key="relation" align="center" width={120}/>
+            <AntdColumn title="部门名称" dataIndex="dept_name" key="dept_name"/>
+            {/* <AntdColumn title="关联用户数量" dataIndex="relation" key="relation" align="center" width={120}/> */}
             <AntdColumn title="操作" key="action" align="center" width={100} render={(_: any, record: any) => (
               <AntdSpace size="middle">
                 {!(record.children?.length > 0) && (
@@ -146,49 +143,6 @@ const DeptsPage = () => {
               </AntdSpace>
             )}/>
           </AntdTable>
-          {/* <div className='flex min-w-[480px] items-center border-b border-divider-regular py-[7px]'> */}
-          {/*  <div className='system-xs-medium-uppercase shrink-0 grow text-text-tertiary'>部门名称</div> */}
-          {/*  <div className='system-xs-medium-uppercase shrink-0 w-[200px] text-center text-text-tertiary'>关联用户数量</div> */}
-          {/*  <div className='system-xs-medium-uppercase w-[200px] shrink-0 px-3 text-center text-text-tertiary'>操作</div> */}
-          {/* </div> */}
-          {/* <div className='relative min-w-[480px]'> */}
-          {/*  { */}
-          {/*    deptList.map((dept: any) => ( */}
-          {/*      <div key={dept.id} className='flex justify-between border-b border-divider-subtle'> */}
-          {/*        <div className='system-sm-regular shrink-0 grow py-2 text-text-secondary'>{dept.name}</div> */}
-          {/*        <div className='system-sm-regular shrink-0 w-[200px] py-2 text-center text-text-secondary'>0</div> */}
-          {/*        <div className='flex w-[200px] shrink-0 items-center justify-center'> */}
-          {/*          <Button variant='ghost-accent' size='small' className={cn('shrink-0')} disabled={!isCurrentWorkspaceManager || isMemberFull} */}
-          {/*            onClick={() => { */}
-          {/*              setTransfer({ */}
-          {/*                mode: 'user', */}
-          {/*                row: JSON.parse(JSON.stringify(dept)), */}
-          {/*              }) */}
-          {/*              setUserModalVisible(true) */}
-          {/*            }}> */}
-          {/*            关联用户 */}
-          {/*          </Button> */}
-          {/*          <Button variant='ghost-accent' size='small' className={cn('shrink-0')} disabled={!isCurrentWorkspaceManager || isMemberFull} */}
-          {/*            onClick={() => { */}
-          {/*              setTransfer({ */}
-          {/*                mode: 'edit', */}
-          {/*                row: JSON.parse(JSON.stringify(dept)), */}
-          {/*              }) */}
-          {/*              setDetailModalVisible(true) */}
-          {/*            }}> */}
-          {/*            编辑 */}
-          {/*          </Button> */}
-          {/*          <Button variant='ghost' size='small' className={cn('shrink-0 text-red-600')} disabled={!isCurrentWorkspaceManager || isMemberFull} onClick={() => { */}
-          {/*            setRow(dept) */}
-          {/*            setShowConfirmDelete(true) */}
-          {/*          }}> */}
-          {/*            刪除 */}
-          {/*          </Button> */}
-          {/*        </div> */}
-          {/*      </div> */}
-          {/*    )) */}
-          {/*  } */}
-          {/* </div> */}
         </div>
       </div>
       {
@@ -217,7 +171,7 @@ const DeptsPage = () => {
       {showConfirmDelete && (
         <Confirm
           title="删除确认"
-          content={`请确认是否删除${row.name}?`}
+          content={`请确认是否删除${row.dept_name}?`}
           isShow={showConfirmDelete}
           onConfirm={handleDel}
           onCancel={() => setShowConfirmDelete(false)}

+ 95 - 123
web/app/components/header/account-setting/dept-page/user-modal.tsx

@@ -1,141 +1,108 @@
 'use client'
-import React, { useCallback, useEffect, useState } from 'react'
-import { RiAddLine, RiCloseLine, RiRefreshLine, RiSearchLine } from '@remixicon/react'
+import React, { useEffect, useState } from 'react'
+import { RiAddLine, RiCloseLine } from '@remixicon/react'
 import Modal from '@/app/components/base/modal'
 import Button from '@/app/components/base/button'
-import { delCorpus, fetchIntentType, fetchTypes } from '@/service/common'
+import { deptAddUsers, deptDelUsers, fetchIntent, fetchNoDeptUsers } from '@/service/common'
 import 'react-multi-email/dist/style.css'
-import Input from '@/app/components/base/input'
-import useSWR from 'swr'
 import cn from '@/utils/classnames'
 import Confirm from '@/app/components/base/confirm'
-import Pagination from '@/app/components/base/pagination'
-import { SimpleSelect } from '@/app/components/base/select'
+import { Select as AntdSelect } from 'antd'
 
-const TypeModal = ({
+const UserModal = ({
   onCancel,
   onSend,
+  transfer,
 }: any) => {
-  const [page, setPage] = React.useState<number>(0)
-  const [limit, setLimit] = useState<number>(10)
-  const [name, setName] = useState('')
-  const [query, setQuery] = useState<any>({})
-  const { data, mutate }: any = useSWR(
-    {
-      url: '/xxx',
-      params: {
-        page: page + 1,
-        limit,
-        ...query,
-      },
-    },
-    fetchIntentType,
-  )
-  const list: any = data?.data || []
-  const total = data?.total || 0
-  const handleSearch = (reset = false) => {
-    if (reset)
-      setIntentType('')
-
-    const params: any = {}
-    if (intentType)
-      params.intentType = intentType
-    setQuery(params)
-    setPage(0)
+  const [list, setList] = useState<any>([])
+  const [total, setTotal] = useState(0)
+  const handlePage = () => {
+    fetchIntent({
+      url: `/dept/dept-accounts/${transfer.row.dept_id}`,
+      params: {},
+    }).then((res: any) => {
+      setList(res.data)
+      setTotal(res.data.length)
+    })
   }
   useEffect(() => {
-    mutate()
-  }, [page, limit])
+    handlePage()
+  }, [])
   const [showConfirmDelete, setShowConfirmDelete] = useState(false)
   const [row, setRow] = useState<any>({})
   const handleDel = async () => {
     try {
-      await delCorpus({
-        url: `/tags/${row.id}`,
-        body: {},
+      const { result }: any = await deptDelUsers({
+        url: '/dept/dept-accounts',
+        body: { dept_id: transfer.row.dept_id, account_ids: [{ account_id: row.account_id }] },
       })
-      setShowConfirmDelete(false)
+      if (result === 'success') {
+        setShowConfirmDelete(false)
+        handlePage()
+      }
     }
     catch (e) { }
   }
   const [showDetail, setShowDetail] = useState(false)
-  const [mode, setMode] = useState<any>('add')
-  const [editIntentType, setEditIntentType] = useState('')
-  const [selectUser, setSelectUser] = useState<any>('')
+  const [selectUser, setSelectUser] = useState<any>([])
   const [userOptions, setUserOptions] = useState<any>([])
   useEffect(() => {
-    fetchTypes({
-      url: '/tags/page',
-      params: {
-        page: 1,
-        limit: 99999,
-        tag_type: 'knowledge_category',
-      },
+    fetchNoDeptUsers({
+      url: '/account/nodept',
+      params: {},
     }).then((res: any) => {
-      setUserOptions(res.data.map((v: any) => ({ name: v.name, value: v.id })) || [])
+      setUserOptions(res.data.map((v: any) => ({ label: v.email, value: v.account_id })) || [])
     })
   }, [])
-  const handleSave = useCallback(async () => {
-    // try {
-    //   let res
-    //   if (transfer.mode === 'add') {
-    //     res = await addCorpus({
-    //       url: '/xxx',
-    //       body: { name, type: 'knowledge_category' },
-    //     })
-    //   }
-    //   else {
-    //     res = await editCorpus({
-    //       url: '/xxx',
-    //       body: { name },
-    //     })
-    //   }
-    //   const { id }: any = res
-    //   if (id) {
-    //     onCancel()
-    //     onSend()
-    //   }
-    // }
-    // catch (e) { }
-  }, [mode, editIntentType, onCancel, onSend])
+  const handleSave = async () => {
+    try {
+      const { result }: any = await deptAddUsers({
+        url: '/dept/dept-accounts',
+        body: { dept_id: transfer.row.dept_id, account_ids: selectUser.map((v: any) => ({ account_id: v })) },
+      })
+      if (result === 'success') {
+        setShowDetail(false)
+        handlePage()
+      }
+    }
+    catch (e) { }
+  }
   return (
     <div>
       <Modal overflowVisible isShow onClose={() => { }} className="p-[24px 32px] w-[800px] max-w-[800px]">
         <div className='mb-2 flex justify-between'>
-          <div className='text-xl font-semibold text-text-primary'>关联用户</div>
+          <div className='text-xl font-semibold text-text-primary'>{transfer.row.dept_name}关联用户</div>
           <RiCloseLine className='h-4 w-4 cursor-pointer text-text-tertiary' onClick={onCancel} />
         </div>
         <div className='flex h-[600px] flex-col'>
-          <div className="flex items-center gap-2">
-            <div className="flex shrink-0 items-center text-gray-500">
-              用户名称
-              <Input
-                className="ml-2"
-                showClearIcon
-                wrapperClassName='!w-[200px]'
-                value={name}
-                onChange={e => setName(e.target.value)}
-                onClear={() => setName('')}
-              />
-            </div>
-            <Button variant='primary' className={cn('ml-auto shrink-0')} onClick={() => {
-              handleSearch(false)
-            }}>
-              <RiSearchLine className='mr-1 h-4 w-4' />
-              搜索
-            </Button>
-            <Button variant='primary' className={cn('shrink-0')} onClick={() => {
-              handleSearch(true)
-            }}>
-              <RiRefreshLine className='mr-1 h-4 w-4' />
-              重置
-            </Button>
-          </div>
+          {/* <div className="flex items-center gap-2"> */}
+          {/*  <div className="flex shrink-0 items-center text-gray-500"> */}
+          {/*    用户名称 */}
+          {/*    <Input */}
+          {/*      className="ml-2" */}
+          {/*      showClearIcon */}
+          {/*      wrapperClassName='!w-[200px]' */}
+          {/*      value={name} */}
+          {/*      onChange={e => setName(e.target.value)} */}
+          {/*      onClear={() => setName('')} */}
+          {/*    /> */}
+          {/*  </div> */}
+          {/*  <Button variant='primary' className={cn('ml-auto shrink-0')} onClick={() => { */}
+          {/*    handleSearch(false) */}
+          {/*  }}> */}
+          {/*    <RiSearchLine className='mr-1 h-4 w-4' /> */}
+          {/*    搜索 */}
+          {/*  </Button> */}
+          {/*  <Button variant='primary' className={cn('shrink-0')} onClick={() => { */}
+          {/*    handleSearch(true) */}
+          {/*  }}> */}
+          {/*    <RiRefreshLine className='mr-1 h-4 w-4' /> */}
+          {/*    重置 */}
+          {/*  </Button> */}
+          {/* </div> */}
           <div className="mt-2">
             <Button variant='primary' className={cn('shrink-0')}
               onClick={() => {
-                setMode('add')
-                setEditIntentType('')
                 setShowDetail(true)
               }}>
               <RiAddLine className='mr-1 h-4 w-4' />
@@ -148,7 +115,8 @@ const TypeModal = ({
                 <table className={'mt-3 w-full min-w-[700px] max-w-full border-collapse border-0 text-sm'}>
                   <thead className="h-8 border-b border-divider-subtle text-xs font-medium uppercase leading-8 text-text-tertiary">
                     <tr>
-                      <td>用户名称</td>
+                      {/* <td>用户名称</td> */}
+                      <td>用户账号</td>
                       <td className="w-[120px] text-center">操作</td>
                     </tr>
                   </thead>
@@ -158,7 +126,8 @@ const TypeModal = ({
                         key={item.id}
                         className={'h-8 border-b border-divider-subtle hover:bg-background-default-hover'}
                       >
-                        <td>{item.name}</td>
+                        {/* <td>{item.name}</td> */}
+                        <td>{item.email}</td>
                         <td className="flex justify-center gap-2">
                           <Button variant='ghost' size='small' className={cn('shrink-0 text-red-600')} onClick={() => {
                             setRow(item)
@@ -173,24 +142,24 @@ const TypeModal = ({
                 </table>
               </div>
               {/* Show Pagination only if the total is more than the limit */}
-              {total && (
-                <Pagination
-                  total={total}
-                  limit={limit}
-                  onLimitChange={setLimit}
-                  current={page}
-                  onChange={setPage}
-                  className='w-full shrink-0 px-0 pb-0'
-                />
-              )}
+              {/* {total && ( */}
+              {/*  <Pagination */}
+              {/*    total={total} */}
+              {/*    limit={limit} */}
+              {/*    onLimitChange={setLimit} */}
+              {/*    current={page} */}
+              {/*    onChange={setPage} */}
+              {/*    className='w-full shrink-0 px-0 pb-0' */}
+              {/*  /> */}
+              {/* )} */}
             </div>
           </div>
         </div>
       </Modal>
       {showConfirmDelete && (
         <Confirm
-          title="删除确认"
-          content={`请确认是否删除${row.name}?`}
+          title="取消确认"
+          content={`请确认是否取消关联${row.name}?`}
           isShow={showConfirmDelete}
           onConfirm={handleDel}
           onCancel={() => setShowConfirmDelete(false)}
@@ -209,11 +178,14 @@ const TypeModal = ({
                   选择用户
                 </div>
                 <div className='w-full'>
-                  <SimpleSelect
-                    defaultValue={selectUser}
-                    onSelect={(i) => { setSelectUser(i.value) }}
-                    items={userOptions}
-                    allowSearch={false}
+                  <AntdSelect
+                    className="w-full"
+                    mode="multiple"
+                    showSearch
+                    placeholder="请选择用户"
+                    optionFilterProp="label"
+                    onChange={v => setSelectUser(v)}
+                    options={userOptions}
                   />
                 </div>
               </div>
@@ -221,7 +193,7 @@ const TypeModal = ({
                 tabIndex={0}
                 className='w-full'
                 onClick={handleSave}
-                disabled={!editIntentType.length}
+                disabled={!selectUser.length}
                 variant='primary'
               >
                 保存
@@ -234,4 +206,4 @@ const TypeModal = ({
   )
 }
 
-export default TypeModal
+export default UserModal

+ 1 - 0
web/app/components/header/account-setting/members-page/index.tsx

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

+ 3 - 2
web/app/components/header/account-setting/members-page/operation/index.tsx

@@ -29,17 +29,18 @@ const Operation = ({
     editor: t('common.members.editor'),
     normal: t('common.members.normal'),
     dataset_operator: t('common.members.datasetOperator'),
+    leader: t('common.members.leader'),
   }
   const roleList = useMemo(() => {
     if (operatorRole === 'owner') {
       return [
-        ...['admin', 'editor', 'normal'],
+        ...['admin', 'leader', 'editor', 'normal'],
         ...(datasetOperatorEnabled ? ['dataset_operator'] : []),
       ]
     }
     if (operatorRole === 'admin') {
       return [
-        ...['editor', 'normal'],
+        ...['leader', 'editor', 'normal'],
         ...(datasetOperatorEnabled ? ['dataset_operator'] : []),
       ]
     }

+ 32 - 10
web/app/components/header/index.tsx

@@ -1,5 +1,5 @@
 'use client'
-import { useCallback, useEffect } from 'react'
+import { useCallback, useEffect, useState } from 'react'
 import Link from 'next/link'
 import { useBoolean } from 'ahooks'
 import { useSelectedLayoutSegment } from 'next/navigation'
@@ -11,10 +11,8 @@ import EnvNav from './env-nav'
 import PluginsNav from './plugins-nav'
 import ExploreNav from './explore-nav'
 import ToolsNav from './tools-nav'
-import { WorkspaceProvider } from '@/context/workspace-context'
 import { useAppContext } from '@/context/app-context'
 import LogoSite from '@/app/components/base/logo/logo-site'
-import WorkplaceSelector from '@/app/components/header/account-dropdown/workplace-selector'
 import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
 import { useProviderContext } from '@/context/provider-context'
 import { useModalContext } from '@/context/modal-context'
@@ -22,6 +20,7 @@ import PlanBadge from './plan-badge'
 import LicenseNav from './license-env'
 import { Plan } from '../billing/type'
 import SkillNav from './skill-nav'
+import { fetchDepts } from '@/service/common'
 
 const navClassName = `
   flex items-center relative mr-0 sm:mr-3 px-3 h-8 rounded-xl
@@ -30,7 +29,7 @@ const navClassName = `
 `
 
 const Header = () => {
-  const { isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator } = useAppContext()
+  const { isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator, userProfile } = useAppContext()
   const selectedSegment = useSelectedLayoutSegment()
   const media = useBreakpoints()
   const isMobile = media === MediaType.mobile
@@ -49,6 +48,28 @@ const Header = () => {
     hideNavMenu()
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [selectedSegment])
+  const [deptMap, setDeptMap] = useState<any>(new Map())
+  const addDeptMap = (key: any, value: any) => {
+    setDeptMap((prevMap: any) => {
+      const newMap = new Map(prevMap)
+      newMap.set(key, value)
+      return newMap
+    })
+  }
+  const formatDept = (depts: any) => {
+    depts.forEach((v: any) => {
+      addDeptMap(v.dept_id, v.dept_name)
+      if (v.children?.length > 0)
+        formatDept(v.children)
+    })
+  }
+  useEffect(() => {
+    fetchDepts({
+      url: '/depts',
+    }).then((res: any) => {
+      formatDept(res.data || [])
+    })
+  }, [])
   return (
     <div className='flex flex-1 items-center justify-between bg-background-body px-4'>
       <div className='flex items-center'>
@@ -65,12 +86,13 @@ const Header = () => {
               <LogoSite className='object-contain' />
             </Link>
             <div className='font-light text-divider-deep'>/</div>
-            <div className='flex items-center gap-0.5'>
-              <WorkspaceProvider>
-                <WorkplaceSelector />
-              </WorkspaceProvider>
-              {enableBilling ? <PlanBadge allowHover sandboxAsUpgrade plan={plan.type} onClick={handlePlanClick} /> : <LicenseNav />}
-            </div>
+            {/* <div className='flex items-center gap-0.5'> */}
+            {/*  <WorkspaceProvider> */}
+            {/*    <WorkplaceSelector /> */}
+            {/*  </WorkspaceProvider> */}
+            {/*  {enableBilling ? <PlanBadge allowHover sandboxAsUpgrade plan={plan.type} onClick={handlePlanClick} /> : <LicenseNav />} */}
+            {/* </div> */}
+            <div className="text-gray-500">{deptMap.get(userProfile.dept_id) || userProfile.dept_id}</div>
           </div>
         }
       </div >

+ 2 - 0
web/i18n/zh-Hans/common.ts

@@ -230,6 +230,8 @@ const translation = {
     owner: '所有者',
     admin: '管理员',
     adminTip: '能够建立应用程序和管理团队设置',
+    leader: '部门领导',
+    leaderTip: '能够对部门内数据进行管理',
     normal: '成员',
     normalTip: '只能使用应用程序,不能建立应用程序',
     editor: '编辑',

+ 2 - 1
web/models/common.ts

@@ -31,6 +31,7 @@ export type UserProfileResponse = {
   last_active_at?: string
   last_login_ip?: string
   created_at?: string
+  dept_id?: string
 }
 
 export type UserProfileOriginResponse = {
@@ -127,7 +128,7 @@ export type IWorkspace = {
 }
 
 export type ICurrentWorkspace = Omit<IWorkspace, 'current'> & {
-  role: 'owner' | 'admin' | 'editor' | 'dataset_operator' | 'normal'
+  role: 'owner' | 'admin' | 'leader' | 'editor' | 'dataset_operator' | 'normal'
   providers: Provider[]
   trial_end_reason?: string
   custom_config?: {

+ 5 - 1
web/models/datasets.ts

@@ -68,6 +68,9 @@ export type DataSet = {
   }
   built_in_field_enabled: boolean
   doc_metadata?: MetadataInDoc[],
+  has_edit_permission: boolean
+  dept_id: string
+  edit_auth: number
 }
 
 export type ExternalAPIItem = {
@@ -159,7 +162,8 @@ export type FetchDatasetsParams = {
     keyword?: string,
     category_ids?: string[],
     type?: string,
-    dept?: string,
+    creatorDept?: string,
+    authType?: string,
   }
 }
 

+ 48 - 66
web/service/common.ts

@@ -576,73 +576,55 @@ export const delBatchCorpusQuestion = ({ url, body }: any) => {
 
 export const fetchDepts = ({ url, params }: any) => {
   console.log('查询部门列表', params, url)
-  // return get(url, { params })
-  return new Promise((resolve) => {
-    setTimeout(() => {
-      const arr: any = []
-      for (let i = 1; i < 3; i++) {
-        const dept1: any = {
-          id: `${i}`,
-          name: `部门_${i}`,
-          children: [],
-        }
-        for (let j = 1; j < 4; j++) {
-          const dept2: any = {
-            id: `${i}_${j}`,
-            name: `部门_${i}-${j}`,
-            children: [],
-          }
-          for (let k = 1; k < 5; k++) {
-            const dept3: any = {
-              id: `${i}_${j}_${k}`,
-              name: `部门_${i}-${j}-${k}`,
-              relation: k % 2,
-            }
-            dept2.children.push(dept3)
-          }
-          dept1.children.push(dept2)
-        }
-        arr.push(dept1)
-      }
-      resolve({
-        data: arr,
-      })
-    }, 1000)
-  })
+  return get(url, { params })
+}
+
+export const addDept = ({ url, body }: any) => {
+  console.log('新增部门', url, body)
+  return post(url, { body })
+}
+
+export const editDept = ({ url, body }: any) => {
+  console.log('编辑部门', url, body)
+  return post(url, { body })
+}
+
+export const delDept = ({ url, body }: any) => {
+  console.log('删除部门', url, body)
+  return del(url, { body })
 }
+
 export const fetchDeptUsers = ({ url, params }: any) => {
   console.log('查询部门用户列表', params, url)
-  // return get(url, { params })
-  return new Promise((resolve) => {
-    setTimeout(() => {
-      const arr: any = []
-      for (let i = 1; i < 3; i++) {
-        const dept1: any = {
-          id: `${i}`,
-          name: `部门_${i}`,
-          children: [],
-        }
-        for (let j = 1; j < 4; j++) {
-          const dept2: any = {
-            id: `${i}_${j}`,
-            name: `部门_${i}-${j}`,
-            children: [],
-          }
-          for (let k = 1; k < 5; k++) {
-            const dept3: any = {
-              id: `${i}_${j}_${k}`,
-              name: `用户_${i}-${j}-${k}`,
-              relation: k % 2,
-            }
-            dept2.children.push(dept3)
-          }
-          dept1.children.push(dept2)
-        }
-        arr.push(dept1)
-      }
-      resolve({
-        data: arr,
-      })
-    }, 1000)
-  })
+  return get(url, { params })
+}
+
+export const fetchNoDeptUsers = ({ url, params }: any) => {
+  console.log('查询未绑定部门用户列表', params, url)
+  return get(url, { params })
+}
+
+export const deptAddUsers = ({ url, body }: any) => {
+  console.log('部门关联用户', url, body)
+  return post(url, { body })
+}
+
+export const deptDelUsers = ({ url, body }: any) => {
+  console.log('部门删除用户', url, body)
+  return del(url, { body })
+}
+
+export const fetchDeptUserTree = ({ url, params }: any) => {
+  console.log('部门账号树', url, params)
+  return get(url, { params })
+}
+
+export const fetchDatasetsPermission = ({ url, params }: any) => {
+  console.log('查询知识库权限', url, params)
+  return get(url, { params })
+}
+
+export const setDatasetsPermission = ({ url, body }: any) => {
+  console.log('配置知识库权限', url, body)
+  return post(url, { body })
 }