V4.8.18 feature (#3565)
* feat: org CRUD (#3380) * feat: add org schema * feat: org manage UI * feat: OrgInfoModal * feat: org tree view * feat: org management * fix: init root org * feat: org permission for app * feat: org support for dataset * fix: disable org role control * styles: opt type signatures * fix: remove unused permission * feat: delete org collaborator * perf: Team org ui (#3499) * perf: org ui * perf: org ui * feat: org auth for app & dataset (#3498) * feat: auth org resource permission * feat: org auth support for app & dataset * perf: org permission check (#3500) * i18n (#3501) * name * i18n * feat: support dataset changeOwner (#3483) * feat: support dataset changeOwner * chore: update dataset change owner api * feat: permission manage UI for org (#3503) * perf: password check;perf: image upload check;perf: sso login check (#3509) * perf: password check * perf: image upload check * perf: sso login check * force show update notification modal & fix login page text (#3512) * fix login page English text * update notification modal * perf: notify account (#3515) * perf(plugin): improve searXNG empty result handling and documentation (#3507) * perf(plugin): improve searXNG empty result handling and documentation * 修改了文档和代码部分无搜索的结果的反馈 * refactor: org pathId (#3516) * optimize payment process (#3517) * feat: support wecom sso (#3518) * feat: support wecom sso * chore: remove unused wecom js-sdk dependency * fix qrcode script (#3520) * fix qrcode script * i18n * perf: full text collection and search code;perf: rename function (#3519) * perf: full text collection and search code * perf: rename function * perf: notify modal * remove invalid code * perf: sso login * perf: pay process * 4.8.18 test (#3524) * perf: remove local token * perf: index * perf: file encoding;perf: leave team code;@c121914yu perf: full text search code (#3528) * perf: text encoding * perf: leave team code * perf: full text search code * fix: http status * perf: embedding search and vector avatar * perf: async read file (#3531) * refactor: team permission manager (#3535) * perf: classify org, group and member * refactor: team per manager * fix: missing functions * 4.8.18 test (#3543) * perf: login check * doc * perf: llm model config * perf: team clb config * fix: MemberModal UI (#3553) * fix: adapt MemberModal title and icon * fix: adapt member modal * fix: search input placeholder * fix: add button text * perf: org permission (#3556) * docs:用户答疑的官方文档补充 (#3540) * docs:用户答疑的官方文档补充 * 问题回答的内容修补 * share link random avatar (#3541) * share link random avatar * fix * delete unused code * share page avatar (#3558) * feat: init 4818 * share page avatar * feat: tmp upgrade code (#3559) * feat: tmp upgrade code * fulltext search test * update action * full text tmp code (#3561) * full text tmp code * fix: init * fix: init * remove tmp code * remove tmp code * 4818-alpha * 4.8.18 test (#3562) * full text tmp code * fix: init * upgrade code * account log * account log * perf: dockerfile * upgrade code * chore: update docs app template submission (#3564) --------- Co-authored-by: a.e. <49438478+I-Info@users.noreply.github.com> Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com> Co-authored-by: heheer <heheer@sealos.io> Co-authored-by: Jiangween <145003935+Jiangween@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,512 @@
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { ChevronDownIcon } from '@chakra-ui/icons';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Checkbox,
|
||||
Flex,
|
||||
Grid,
|
||||
HStack,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
Tag,
|
||||
Text
|
||||
} from '@chakra-ui/react';
|
||||
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
|
||||
import MyAvatar from '@fastgpt/web/components/common/Avatar';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
import PermissionSelect from './PermissionSelect';
|
||||
import PermissionTags from './PermissionTags';
|
||||
import {
|
||||
DEFAULT_ORG_AVATAR,
|
||||
DEFAULT_TEAM_AVATAR,
|
||||
DEFAULT_USER_AVATAR
|
||||
} from '@fastgpt/global/common/system/constants';
|
||||
import Path from '@/components/common/folder/Path';
|
||||
import { getOrgChildrenPath } from '@fastgpt/global/support/user/team/org/constant';
|
||||
import { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type';
|
||||
import { OrgType } from '@fastgpt/global/support/user/team/org/type';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { CollaboratorContext } from './context';
|
||||
|
||||
const HoverBoxStyle = {
|
||||
bgColor: 'myGray.50',
|
||||
cursor: 'pointer'
|
||||
};
|
||||
|
||||
function MemberModal({
|
||||
onClose,
|
||||
addPermissionOnly: addOnly = false
|
||||
}: {
|
||||
onClose: () => void;
|
||||
addPermissionOnly?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { userInfo, loadAndGetTeamMembers, loadAndGetGroups, loadAndGetOrgs } = useUserStore();
|
||||
|
||||
const collaboratorList = useContextSelector(CollaboratorContext, (v) => v.collaboratorList);
|
||||
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
const [filterClass, setFilterClass] = useState<'member' | 'org' | 'group'>();
|
||||
|
||||
const { data: [members = [], groups = [], orgs = []] = [], loading: loadingMembersAndGroups } =
|
||||
useRequest2(
|
||||
async () => {
|
||||
if (!userInfo?.team?.teamId) return [[], []];
|
||||
return Promise.all([
|
||||
loadAndGetTeamMembers(true),
|
||||
loadAndGetGroups(true),
|
||||
loadAndGetOrgs(true)
|
||||
]);
|
||||
},
|
||||
{
|
||||
manual: false,
|
||||
refreshDeps: [userInfo?.team?.teamId]
|
||||
}
|
||||
);
|
||||
|
||||
const [parentPath, setParentPath] = useState('');
|
||||
const paths = useMemo(() => {
|
||||
const splitPath = parentPath.split('/').filter(Boolean);
|
||||
return splitPath
|
||||
.map((id) => {
|
||||
const org = orgs.find((org) => org.pathId === id)!;
|
||||
|
||||
if (org.path === '') return;
|
||||
|
||||
return {
|
||||
parentId: getOrgChildrenPath(org),
|
||||
parentName: org.name
|
||||
};
|
||||
})
|
||||
.filter(Boolean) as ParentTreePathItemType[];
|
||||
}, [parentPath, orgs]);
|
||||
|
||||
const [selectedOrgIdList, setSelectedOrgIdList] = useState<string[]>([]);
|
||||
const currentOrg = useMemo(() => {
|
||||
const splitPath = parentPath.split('/');
|
||||
const currentOrgId = splitPath[splitPath.length - 1];
|
||||
if (!currentOrgId) return;
|
||||
|
||||
return orgs.find((org) => org.pathId === currentOrgId);
|
||||
}, [orgs, parentPath]);
|
||||
const filterOrgs: (OrgType & { count?: number })[] = useMemo(() => {
|
||||
if (searchText) return orgs.filter((item) => item.name.includes(searchText));
|
||||
if (!searchText && filterClass !== 'org') return [];
|
||||
if (parentPath === '') {
|
||||
setParentPath(`/${orgs[0].pathId}`);
|
||||
return [];
|
||||
}
|
||||
return orgs
|
||||
.filter((org) => org.path === parentPath)
|
||||
.map((item) => ({
|
||||
...item,
|
||||
count:
|
||||
item.members.length + orgs.filter((org) => org.path === getOrgChildrenPath(item)).length
|
||||
}));
|
||||
}, [orgs, searchText, filterClass, parentPath]);
|
||||
|
||||
const [selectedMemberIdList, setSelectedMembers] = useState<string[]>([]);
|
||||
const filterMembers = useMemo(() => {
|
||||
if (searchText) return members.filter((item) => item.memberName.includes(searchText));
|
||||
if (!searchText && filterClass !== 'member' && filterClass !== 'org') return [];
|
||||
|
||||
if (currentOrg && filterClass === 'org') {
|
||||
return members.filter((item) => currentOrg.members.find((v) => v.tmbId === item.tmbId));
|
||||
}
|
||||
|
||||
return members;
|
||||
}, [members, searchText, filterClass, currentOrg]);
|
||||
|
||||
const [selectedGroupIdList, setSelectedGroupIdList] = useState<string[]>([]);
|
||||
const filterGroups = useMemo(() => {
|
||||
if (searchText) return groups.filter((item) => item.name.includes(searchText));
|
||||
if (!searchText && filterClass !== 'group') return [];
|
||||
|
||||
return groups;
|
||||
}, [groups, searchText, filterClass]);
|
||||
|
||||
const permissionList = useContextSelector(CollaboratorContext, (v) => v.permissionList);
|
||||
const getPerLabelList = useContextSelector(CollaboratorContext, (v) => v.getPerLabelList);
|
||||
const [selectedPermission, setSelectedPermission] = useState<number | undefined>(
|
||||
permissionList?.read?.value
|
||||
);
|
||||
const perLabel = useMemo(() => {
|
||||
if (selectedPermission === undefined) return '';
|
||||
return getPerLabelList(selectedPermission!).join('、');
|
||||
}, [getPerLabelList, selectedPermission]);
|
||||
|
||||
const onUpdateCollaborators = useContextSelector(
|
||||
CollaboratorContext,
|
||||
(v) => v.onUpdateCollaborators
|
||||
);
|
||||
const { runAsync: onConfirm, loading: isUpdating } = useRequest2(
|
||||
() =>
|
||||
onUpdateCollaborators({
|
||||
members: selectedMemberIdList,
|
||||
groups: selectedGroupIdList,
|
||||
orgs: selectedOrgIdList,
|
||||
permission: selectedPermission!
|
||||
}),
|
||||
{
|
||||
successToast: t('common:common.Add Success'),
|
||||
onSuccess() {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const entryList = useRef([
|
||||
{ label: t('user:team.group.members'), icon: DEFAULT_USER_AVATAR, value: 'member' },
|
||||
{ label: t('user:team.org.org'), icon: DEFAULT_ORG_AVATAR, value: 'org' },
|
||||
{ label: t('user:team.group.group'), icon: DEFAULT_TEAM_AVATAR, value: 'group' }
|
||||
]);
|
||||
|
||||
const selectedList = useMemo(() => {
|
||||
const selectedOrgs = orgs.filter((org) => selectedOrgIdList.includes(org._id));
|
||||
const selectedGroups = groups.filter((group) => selectedGroupIdList.includes(group._id));
|
||||
const selectedMembers = members.filter((member) => selectedMemberIdList.includes(member.tmbId));
|
||||
|
||||
return [
|
||||
...selectedOrgs.map((item) => ({
|
||||
id: `org-${item._id}`,
|
||||
avatar: item.avatar,
|
||||
name: item.name,
|
||||
onDelete: () => setSelectedOrgIdList(selectedOrgIdList.filter((v) => v !== item._id))
|
||||
})),
|
||||
...selectedGroups.map((item) => ({
|
||||
id: `group-${item._id}`,
|
||||
avatar: item.avatar,
|
||||
name: item.name === DefaultGroupName ? userInfo?.team.teamName : item.name,
|
||||
onDelete: () => setSelectedGroupIdList(selectedGroupIdList.filter((v) => v !== item._id))
|
||||
})),
|
||||
...selectedMembers.map((item) => ({
|
||||
id: `member-${item.tmbId}`,
|
||||
avatar: item.avatar,
|
||||
name: item.memberName,
|
||||
onDelete: () => setSelectedMembers(selectedMemberIdList.filter((v) => v !== item.tmbId))
|
||||
}))
|
||||
];
|
||||
}, [
|
||||
orgs,
|
||||
groups,
|
||||
members,
|
||||
selectedOrgIdList,
|
||||
selectedGroupIdList,
|
||||
selectedMemberIdList,
|
||||
userInfo?.team.teamName
|
||||
]);
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen
|
||||
onClose={onClose}
|
||||
iconSrc={addOnly ? 'keyPrimary' : 'modal/AddClb'}
|
||||
title={addOnly ? t('user:team.add_permission') : t('user:team.add_collaborator')}
|
||||
minW="800px"
|
||||
h={'100%'}
|
||||
maxH={'90vh'}
|
||||
isCentered
|
||||
isLoading={loadingMembersAndGroups}
|
||||
>
|
||||
<ModalBody flex={'1'}>
|
||||
<Grid
|
||||
border="1px solid"
|
||||
borderColor="myGray.200"
|
||||
borderRadius="0.5rem"
|
||||
gridTemplateColumns="1fr 1fr"
|
||||
h={'100%'}
|
||||
>
|
||||
<Flex
|
||||
h={'100%'}
|
||||
flexDirection="column"
|
||||
borderRight="1px solid"
|
||||
borderColor="myGray.200"
|
||||
p="4"
|
||||
>
|
||||
<SearchInput
|
||||
placeholder={t('user:search_group_org_user')}
|
||||
bgColor="myGray.50"
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
/>
|
||||
|
||||
<Flex flexDirection="column" mt="3" overflow={'auto'} flex={'1 0 0'} h={0}>
|
||||
{!searchText && !filterClass && (
|
||||
<>
|
||||
{entryList.current.map((item) => {
|
||||
return (
|
||||
<HStack
|
||||
key={item.value}
|
||||
justifyContent="space-between"
|
||||
py="2"
|
||||
px="3"
|
||||
borderRadius="sm"
|
||||
alignItems="center"
|
||||
_hover={HoverBoxStyle}
|
||||
_notLast={{ mb: 1 }}
|
||||
onClick={() => setFilterClass(item.value as any)}
|
||||
>
|
||||
<MyAvatar src={item.icon} w="1.5rem" borderRadius={'50%'} />
|
||||
<Box ml="2" w="full">
|
||||
{item.label}
|
||||
</Box>
|
||||
<MyIcon name="core/chat/chevronRight" w="16px" />
|
||||
</HStack>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Path */}
|
||||
{!searchText && filterClass && (
|
||||
<Box mb={1}>
|
||||
<Path
|
||||
paths={[
|
||||
{
|
||||
parentId: filterClass,
|
||||
parentName:
|
||||
filterClass === 'member'
|
||||
? t('user:team.group.members')
|
||||
: filterClass === 'org'
|
||||
? t('user:team.org.org')
|
||||
: t('user:team.group.group')
|
||||
},
|
||||
...paths
|
||||
]}
|
||||
onClick={(parentId) => {
|
||||
if (parentId === '') {
|
||||
setFilterClass(undefined);
|
||||
setParentPath('');
|
||||
} else if (
|
||||
parentId === 'member' ||
|
||||
parentId === 'org' ||
|
||||
parentId === 'group'
|
||||
) {
|
||||
setFilterClass(parentId);
|
||||
setParentPath('');
|
||||
} else {
|
||||
setParentPath(parentId);
|
||||
}
|
||||
}}
|
||||
rootName={t('common:common.Team')}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Flex flexDirection={'column'} gap={1} userSelect={'none'}>
|
||||
{filterMembers.map((member) => {
|
||||
const collaborator = collaboratorList?.find((v) => v.tmbId === member.tmbId);
|
||||
const disabled = addOnly && collaborator !== undefined;
|
||||
const onChange = () => {
|
||||
if (disabled) return;
|
||||
setSelectedMembers((state) => {
|
||||
if (state.includes(member.tmbId)) {
|
||||
return state.filter((v) => v !== member.tmbId);
|
||||
}
|
||||
return [...state, member.tmbId];
|
||||
});
|
||||
};
|
||||
return (
|
||||
<HStack
|
||||
justifyContent="space-between"
|
||||
key={member.tmbId}
|
||||
py="2"
|
||||
px="3"
|
||||
borderRadius="sm"
|
||||
alignItems="center"
|
||||
_hover={HoverBoxStyle}
|
||||
onClick={onChange}
|
||||
>
|
||||
<Checkbox
|
||||
isDisabled={disabled}
|
||||
isChecked={disabled || selectedMemberIdList.includes(member.tmbId)}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
<MyAvatar src={member.avatar} w="1.5rem" borderRadius={'50%'} />
|
||||
<Box w="full" ml="2">
|
||||
{member.memberName}
|
||||
</Box>
|
||||
<PermissionTags
|
||||
permission={addOnly ? undefined : collaborator?.permission.value}
|
||||
/>
|
||||
</HStack>
|
||||
);
|
||||
})}
|
||||
{filterOrgs.map((org) => {
|
||||
const collaborator = collaboratorList?.find((v) => v.orgId === org._id);
|
||||
const disabled = addOnly && collaborator !== undefined;
|
||||
const onChange = () => {
|
||||
if (disabled) return;
|
||||
setSelectedOrgIdList((state) => {
|
||||
if (state.includes(org._id)) {
|
||||
return state.filter((v) => v !== org._id);
|
||||
}
|
||||
return [...state, org._id];
|
||||
});
|
||||
};
|
||||
return (
|
||||
<HStack
|
||||
justifyContent="space-between"
|
||||
key={org._id}
|
||||
py="2"
|
||||
px="3"
|
||||
borderRadius="sm"
|
||||
alignItems="center"
|
||||
_hover={HoverBoxStyle}
|
||||
onClick={onChange}
|
||||
>
|
||||
<Checkbox
|
||||
isDisabled={disabled}
|
||||
isChecked={disabled || selectedOrgIdList.includes(org._id)}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
<MyAvatar src={org.avatar} w="1.5rem" borderRadius={'50%'} />
|
||||
<HStack ml="2" w="full" gap="5px">
|
||||
<Text>{org.name}</Text>
|
||||
{org.count && (
|
||||
<Tag size="sm" my="auto">
|
||||
{org.count}
|
||||
</Tag>
|
||||
)}
|
||||
</HStack>
|
||||
<PermissionTags
|
||||
permission={addOnly ? undefined : collaborator?.permission.value}
|
||||
/>
|
||||
{org.count && (
|
||||
<MyIcon
|
||||
name="core/chat/chevronRight"
|
||||
w="16px"
|
||||
p="4px"
|
||||
rounded={'6px'}
|
||||
_hover={{
|
||||
bgColor: 'myGray.200'
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setParentPath(getOrgChildrenPath(org));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
);
|
||||
})}
|
||||
{filterGroups.map((group) => {
|
||||
const collaborator = collaboratorList?.find((v) => v.groupId === group._id);
|
||||
const disabled = addOnly && collaborator !== undefined;
|
||||
const onChange = () => {
|
||||
if (disabled) return;
|
||||
setSelectedGroupIdList((state) => {
|
||||
if (state.includes(group._id)) {
|
||||
return state.filter((v) => v !== group._id);
|
||||
}
|
||||
return [...state, group._id];
|
||||
});
|
||||
};
|
||||
return (
|
||||
<HStack
|
||||
justifyContent="space-between"
|
||||
key={group._id}
|
||||
py="2"
|
||||
px="3"
|
||||
borderRadius="sm"
|
||||
alignItems="center"
|
||||
_hover={HoverBoxStyle}
|
||||
onClick={onChange}
|
||||
>
|
||||
<Checkbox
|
||||
isDisabled={disabled}
|
||||
isChecked={disabled || selectedGroupIdList.includes(group._id)}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
<MyAvatar src={group.avatar} w="1.5rem" borderRadius={'50%'} />
|
||||
<Box ml="2" w="full">
|
||||
{group.name === DefaultGroupName ? userInfo?.team.teamName : group.name}
|
||||
</Box>
|
||||
<PermissionTags
|
||||
permission={addOnly ? undefined : collaborator?.permission.value}
|
||||
/>
|
||||
</HStack>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<Flex h={'100%'} p="4" flexDirection="column">
|
||||
<Box>
|
||||
{`${t('user:has_chosen')}: `}
|
||||
{selectedMemberIdList.length + selectedGroupIdList.length + selectedOrgIdList.length}
|
||||
</Box>
|
||||
<Flex flexDirection="column" mt="2" gap={1} overflow={'auto'} flex={'1 0 0'} h={0}>
|
||||
{selectedList.map((item) => {
|
||||
return (
|
||||
<HStack
|
||||
justifyContent="space-between"
|
||||
key={item.id}
|
||||
py="2"
|
||||
px="3"
|
||||
borderRadius="sm"
|
||||
alignItems="center"
|
||||
_hover={HoverBoxStyle}
|
||||
>
|
||||
<MyAvatar src={item.avatar} w="1.5rem" borderRadius={'50%'} />
|
||||
<Box w="full" ml="2">
|
||||
{item.name}
|
||||
</Box>
|
||||
<MyIcon
|
||||
name="common/closeLight"
|
||||
w="1rem"
|
||||
cursor={'pointer'}
|
||||
_hover={{
|
||||
color: 'red.600'
|
||||
}}
|
||||
onClick={item.onDelete}
|
||||
/>
|
||||
</HStack>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Grid>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
{!addOnly && !!permissionList && (
|
||||
<PermissionSelect
|
||||
value={selectedPermission}
|
||||
Button={
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
bg={'myGray.50'}
|
||||
border="base"
|
||||
fontSize={'sm'}
|
||||
px={3}
|
||||
borderRadius={'md'}
|
||||
h={'32px'}
|
||||
>
|
||||
{t(perLabel as any)}
|
||||
<ChevronDownIcon fontSize={'md'} />
|
||||
</Flex>
|
||||
}
|
||||
onChange={(v) => setSelectedPermission(v)}
|
||||
/>
|
||||
)}
|
||||
{addOnly && (
|
||||
<HStack bg={'blue.50'} color={'blue.600'} padding={'6px 12px'} rounded={'5px'}>
|
||||
<MyIcon name="common/info" w="1rem" h="1rem" />
|
||||
<Text fontSize="12px">{t('user:permission_add_tip')}</Text>
|
||||
</HStack>
|
||||
)}
|
||||
<Button isLoading={isUpdating} ml="4" h={'32px'} onClick={onConfirm}>
|
||||
{t('common:common.Confirm')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
);
|
||||
}
|
||||
|
||||
export default MemberModal;
|
||||
Reference in New Issue
Block a user