4.8.19-feature (#3636)

* feat: sync org from wecom, pref: member list pagination (#3549)

* feat: sync org

* chore: fe

* chore: loading

* chore: type

* pref: team member list change to pagination. Edit a sort of list apis.

* feat: member update avatar

* chore: user avatar move to tmb

* chore: init scripts move user avatar

* chore: sourceMember

* fix: list api sourceMember

* fix: member sync

* fix: pagination

* chore: adjust code

* chore: move changeOwner to pro

* chore: init v4819 script

* chore: adjust code

* chore: UserBox

* perf: scroll page code

* perf: list data

* docs:更新用户答疑 (#3576)

* docs: add custom uid docs (#3572)

* fix: pagination bug (#3577)

* 4.8.19 test (#3584)

* faet: dataset search filter

* fix: scroll page

* fix: collection list api old version (#3591)

* fix: collection list api format

* fix: type error of addSourceMemeber

* fix: scroll fetch (#3592)

* fix: yuque dataset file folder can enter (#3593)

* perf: load members;perf: yuque load;fix: workflow llm params cannot close (#3594)

* chat openapi doc

* feat: dataset openapi doc

* perf: load members

* perf: member load code

* perf: yuque load

* fix: workflow llm params cannot close

* fix: api dataset reference tag preview (#3600)

* perf: doc

* feat: chat page config

* fix: http parse (#3634)

* update doc

* fix: http parse

* fix code run node reset template (#3633)

Co-authored-by: Archer <545436317@qq.com>

* docs:faq (#3627)

* docs:faq

* docsFix

* perf: sleep plugin

* fix: selector

---------

Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com>
Co-authored-by: Jiangween <145003935+Jiangween@users.noreply.github.com>
Co-authored-by: heheer <heheer@sealos.io>
This commit is contained in:
Archer
2025-01-20 19:42:33 +08:00
committed by GitHub
parent 9f33729ca9
commit 3c97757e4d
170 changed files with 2317 additions and 1615 deletions

View File

@@ -54,7 +54,7 @@ const InputGuideConfig = ({
onChange: (e: ChatInputGuideConfigType) => void;
}) => {
const { t } = useTranslation();
const { chatT, commonT } = useI18n();
const { chatT } = useI18n();
const { isOpen, onOpen, onClose } = useDisclosure();
const {
isOpen: isOpenLexiconConfig,
@@ -220,7 +220,7 @@ const LexiconConfigModal = ({ appId, onClose }: { appId: string; onClose: () =>
});
const { run: createNewData, loading: isCreating } = useRequest2(
(textList: string[]) => {
async (textList: string[]) => {
if (textList.filter(Boolean).length === 0) {
return Promise.resolve();
}

View File

@@ -29,8 +29,6 @@ export type ChatProviderProps = {
outLinkAuthData?: OutLinkChatAuthProps;
chatType: 'log' | 'chat' | 'share' | 'team';
showRawSource: boolean;
showNodeStatus: boolean;
};
type useChatStoreType = ChatProviderProps & {
@@ -132,8 +130,6 @@ const Provider = ({
chatId,
outLinkAuthData = {},
chatType = 'chat',
showRawSource,
showNodeStatus,
children,
...props
}: ChatProviderProps & {
@@ -249,9 +245,7 @@ const Provider = ({
chatId,
outLinkAuthData,
getHistoryResponseData,
chatType,
showRawSource,
showNodeStatus
chatType
};
return <ChatBoxContext.Provider value={value}>{children}</ChatBoxContext.Provider>;

View File

@@ -25,6 +25,7 @@ import { isEqual } from 'lodash';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { formatTimeToChatItemTime } from '@fastgpt/global/common/string/time';
import dayjs from 'dayjs';
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
const colorMap = {
[ChatStatusEnum.loading]: {
@@ -139,7 +140,7 @@ const ChatItem = (props: Props) => {
const isChatting = useContextSelector(ChatBoxContext, (v) => v.isChatting);
const chatType = useContextSelector(ChatBoxContext, (v) => v.chatType);
const showNodeStatus = useContextSelector(ChatBoxContext, (v) => v.showNodeStatus);
const showNodeStatus = useContextSelector(ChatItemContext, (v) => v.showNodeStatus);
const isChatLog = chatType === 'log';
const { copyData } = useCopyData();

View File

@@ -9,19 +9,16 @@ import RawSourceBox from '@/components/core/dataset/RawSourceBox';
import { getWebReqUrl } from '@fastgpt/web/common/system/utils';
import { useContextSelector } from 'use-context-selector';
import { ChatBoxContext } from '../Provider';
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
const QuoteModal = ({
rawSearch = [],
onClose,
canEditDataset,
showRawSource,
chatItemId,
metadata
}: {
rawSearch: SearchDataResponseItemType[];
onClose: () => void;
canEditDataset: boolean;
showRawSource: boolean;
chatItemId: string;
metadata?: {
collectionId: string;
@@ -47,6 +44,11 @@ const QuoteModal = ({
chatItemId,
...(v.outLinkAuthData || {})
}));
const showRawSource = useContextSelector(ChatItemContext, (v) => v.isShowReadRawSource);
const showRouteToDatasetDetail = useContextSelector(
ChatItemContext,
(v) => v.showRouteToDatasetDetail
);
return (
<>
@@ -71,12 +73,7 @@ const QuoteModal = ({
}
>
<ModalBody>
<QuoteList
rawSearch={filterResults}
canEditDataset={canEditDataset}
canViewSource={showRawSource}
chatItemId={chatItemId}
/>
<QuoteList rawSearch={filterResults} chatItemId={chatItemId} />
</ModalBody>
</MyModal>
</>
@@ -87,14 +84,10 @@ export default QuoteModal;
export const QuoteList = React.memo(function QuoteList({
chatItemId,
rawSearch = [],
canEditDataset,
canViewSource
rawSearch = []
}: {
chatItemId?: string;
rawSearch: SearchDataResponseItemType[];
canEditDataset: boolean;
canViewSource: boolean;
}) {
const theme = useTheme();
@@ -104,6 +97,11 @@ export const QuoteList = React.memo(function QuoteList({
chatId: v.chatId,
...(v.outLinkAuthData || {})
}));
const showRawSource = useContextSelector(ChatItemContext, (v) => v.isShowReadRawSource);
const showRouteToDatasetDetail = useContextSelector(
ChatItemContext,
(v) => v.showRouteToDatasetDetail
);
return (
<>
@@ -120,8 +118,8 @@ export const QuoteList = React.memo(function QuoteList({
>
<QuoteItem
quoteItem={item}
canViewSource={canViewSource}
canEditDataset={canEditDataset}
canViewSource={showRawSource}
canEditDataset={showRouteToDatasetDetail}
{...RawSourceBoxProps}
/>
</Box>

View File

@@ -49,7 +49,6 @@ const ResponseTags = ({
const [quoteFolded, setQuoteFolded] = useState<boolean>(true);
const chatType = useContextSelector(ChatBoxContext, (v) => v.chatType);
const showRawSource = useContextSelector(ChatBoxContext, (v) => v.showRawSource);
const notSharePage = useMemo(() => chatType !== 'share', [chatType]);
const {
@@ -251,8 +250,6 @@ const ResponseTags = ({
<QuoteModal
{...quoteModalData}
chatItemId={historyItem.dataId}
canEditDataset={notSharePage}
showRawSource={showRawSource}
onClose={() => setQuoteModalData(undefined)}
/>
)}

View File

@@ -18,7 +18,6 @@ import { Box, Checkbox } from '@chakra-ui/react';
import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus';
import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt';
import { useForm } from 'react-hook-form';
import { useRouter } from 'next/router';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useTranslation } from 'next-i18next';
import {

View File

@@ -249,14 +249,7 @@ export const WholeResponseContent = ({
{activeModule.quoteList && activeModule.quoteList.length > 0 && (
<Row
label={t('common:core.chat.response.module quoteList')}
rawDom={
<QuoteList
canEditDataset
canViewSource
chatItemId={dataId}
rawSearch={activeModule.quoteList}
/>
}
rawDom={<QuoteList chatItemId={dataId} rawSearch={activeModule.quoteList} />}
/>
)}
</>

View File

@@ -5,7 +5,6 @@ import { useTranslation } from 'next-i18next';
import { getCollectionSourceAndOpen } from '@/web/core/dataset/hooks/readCollectionSource';
import { getSourceNameIcon } from '@fastgpt/global/core/dataset/utils';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useI18n } from '@/web/context/I18n';
import type { readCollectionSourceBody } from '@/pages/api/core/dataset/collection/read';
type Props = BoxProps &
@@ -33,7 +32,6 @@ const RawSourceBox = ({
...props
}: Props) => {
const { t } = useTranslation();
const { fileT } = useI18n();
const canPreview = !!sourceId && canView;
@@ -51,7 +49,7 @@ const RawSourceBox = ({
return (
<MyTooltip
label={canPreview ? fileT('click_to_view_raw_source') : ''}
label={canPreview ? t('file:click_to_view_raw_source') : ''}
shouldWrapChildren={false}
>
<Box

View File

@@ -1,4 +1,4 @@
import { useUserStore } from '@/web/support/user/useUserStore';
import { getTeamMembers } from '@/web/support/user/team/api';
import {
Box,
Flex,
@@ -15,6 +15,7 @@ import Icon from '@fastgpt/web/components/common/Icon';
import MyModal from '@fastgpt/web/components/common/MyModal';
import MyTag from '@fastgpt/web/components/common/Tag';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import { useTranslation } from 'next-i18next';
import React, { useState } from 'react';
@@ -31,13 +32,12 @@ export function ChangeOwnerModal({
onChangeOwner
}: ChangeOwnerModalProps & { onClose: () => void }) {
const { t } = useTranslation();
const { loadAndGetTeamMembers } = useUserStore();
const [inputValue, setInputValue] = React.useState('');
const { data: teamMembers = [] } = useRequest2(loadAndGetTeamMembers, {
manual: false
const { data: teamMembers, ScrollData } = useScrollPagination(getTeamMembers, {
pageSize: 15
});
const memberList = teamMembers.filter((item) => {
return item.memberName.includes(inputValue);
});
@@ -101,11 +101,6 @@ export function ChangeOwnerModal({
onOpenMemberListMenu();
setSelectedMember(null);
}}
// onBlur={() => {
// setTimeout(() => {
// onCloseMemberListMenu();
// }, 10);
// }}
{...(selectedMember && { pl: '10' })}
/>
</Flex>
@@ -123,26 +118,28 @@ export function ChangeOwnerModal({
maxH={'300px'}
overflow={'auto'}
>
{memberList.map((item) => (
<Box
key={item.tmbId}
p="2"
_hover={{ bg: 'myGray.100' }}
mx="1"
borderRadius="md"
cursor={'pointer'}
onClickCapture={() => {
setInputValue(item.memberName);
setSelectedMember(item);
onCloseMemberListMenu();
}}
>
<Flex align="center">
<Avatar src={item.avatar} w="1.25rem" />
<Box ml="2">{item.memberName}</Box>
</Flex>
</Box>
))}
<ScrollData>
{memberList.map((item) => (
<Box
key={item.tmbId}
p="2"
_hover={{ bg: 'myGray.100' }}
mx="1"
borderRadius="md"
cursor={'pointer'}
onClickCapture={() => {
setInputValue(item.memberName);
setSelectedMember(item);
onCloseMemberListMenu();
}}
>
<Flex align="center">
<Avatar src={item.avatar} w="1.25rem" />
<Box ml="2">{item.memberName}</Box>
</Flex>
</Box>
))}
</ScrollData>
</Flex>
)}

View File

@@ -33,6 +33,10 @@ 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';
import { getTeamMembers } from '@/web/support/user/team/api';
import { getGroupList } from '@/web/support/user/team/group/api';
import { getOrgList } from '@/web/support/user/team/org/api';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
const HoverBoxStyle = {
bgColor: 'myGray.50',
@@ -47,30 +51,27 @@ function MemberModal({
addPermissionOnly?: boolean;
}) {
const { t } = useTranslation();
const { userInfo, loadAndGetTeamMembers, loadAndGetGroups, loadAndGetOrgs } = useUserStore();
const { userInfo } = useUserStore();
const collaboratorList = useContextSelector(CollaboratorContext, (v) => v.collaboratorList);
const [searchText, setSearchText] = useState<string>('');
const [filterClass, setFilterClass] = useState<'member' | 'org' | 'group'>();
const { data: members, ScrollData } = useScrollPagination(getTeamMembers, {
pageSize: 15
});
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 { data: [groups = [], orgs = []] = [], loading: loadingGroupsAndOrgs } = useRequest2(
async () => {
if (!userInfo?.team?.teamId) return [[], []];
return Promise.all([getGroupList(), getOrgList()]);
},
{
manual: false,
refreshDeps: [userInfo?.team?.teamId]
}
);
const [parentPath, setParentPath] = useState('');
const paths = useMemo(() => {
const splitPath = parentPath.split('/').filter(Boolean);
return splitPath
@@ -212,7 +213,7 @@ function MemberModal({
h={'100%'}
maxH={'90vh'}
isCentered
isLoading={loadingMembersAndGroups}
isLoading={loadingGroupsAndOrgs}
>
<ModalBody flex={'1'}>
<Grid
@@ -236,6 +237,7 @@ function MemberModal({
/>
<Flex flexDirection="column" mt="3" overflow={'auto'} flex={'1 0 0'} h={0}>
{/* Entry */}
{!searchText && !filterClass && (
<>
{entryList.current.map((item) => {
@@ -298,142 +300,135 @@ function MemberModal({
</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>
{filterClass && (
<ScrollData
flexDirection={'column'}
gap={1}
userSelect={'none'}
height={'fit-content'}
>
{filterOrgs.map((org) => {
const onChange = () => {
setSelectedOrgIdList((state) => {
if (state.includes(org._id)) {
return state.filter((v) => v !== org._id);
}
return [...state, org._id];
});
};
const collaborator = collaboratorList?.find((v) => v.orgId === org._id);
return (
<HStack
justifyContent="space-between"
key={org._id}
py="2"
px="3"
borderRadius="sm"
alignItems="center"
_hover={HoverBoxStyle}
onClick={onChange}
>
<Checkbox
isChecked={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={collaborator?.permission.value} />
{org.count && (
<Tag size="sm" my="auto">
{org.count}
</Tag>
<MyIcon
name="core/chat/chevronRight"
w="16px"
p="4px"
rounded={'6px'}
_hover={{
bgColor: 'myGray.200'
}}
onClick={() => {
setParentPath(getOrgChildrenPath(org));
}}
/>
)}
</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));
}}
);
})}
{filterMembers.map((member) => {
const onChange = () => {
setSelectedMembers((state) => {
if (state.includes(member.tmbId)) {
return state.filter((v) => v !== member.tmbId);
}
return [...state, member.tmbId];
});
};
const collaborator = collaboratorList?.find((v) => v.tmbId === member.tmbId);
return (
<HStack
justifyContent="space-between"
key={member.tmbId}
py="2"
px="3"
borderRadius="sm"
alignItems="center"
_hover={HoverBoxStyle}
onClick={onChange}
>
<Checkbox
isChecked={selectedMemberIdList.includes(member.tmbId)}
pointerEvents="none"
/>
)}
</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>
<MyAvatar src={member.avatar} w="1.5rem" borderRadius={'50%'} />
<Box w="full" ml="2">
{member.memberName}
</Box>
<PermissionTags permission={collaborator?.permission.value} />
</HStack>
);
})}
{filterGroups.map((group) => {
const onChange = () => {
setSelectedGroupIdList((state) => {
if (state.includes(group._id)) {
return state.filter((v) => v !== group._id);
}
return [...state, group._id];
});
};
const collaborator = collaboratorList?.find((v) => v.groupId === group._id);
return (
<HStack
justifyContent="space-between"
key={group._id}
py="2"
px="3"
borderRadius="sm"
alignItems="center"
_hover={HoverBoxStyle}
onClick={onChange}
>
<Checkbox
isChecked={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={collaborator?.permission.value} />
</HStack>
);
})}
</ScrollData>
)}
</Flex>
</Flex>

View File

@@ -1,7 +1,7 @@
import { RequestPaging } from '@/types';
import { PaginationProps } from '@fastgpt/web/common/fetch/type';
export type GetAppChatLogsParams = RequestPaging & {
export type GetAppChatLogsParams = PaginationProps<{
appId: string;
dateStart: Date;
dateEnd: Date;
};
}>;

View File

@@ -3,24 +3,24 @@ import {
DatasetCollectionTypeEnum,
DatasetTypeEnum
} from '@fastgpt/global/core/dataset/constants';
import type { RequestPaging } from '@/types';
import { TrainingModeEnum } from '@fastgpt/global/core/dataset/constants';
import type { SearchTestItemType } from '@/types/core/dataset';
import { UploadChunkItemType } from '@fastgpt/global/core/dataset/type';
import { DatasetCollectionSchemaType } from '@fastgpt/global/core/dataset/type';
import { PermissionTypeEnum } from '@fastgpt/global/support/permission/constant';
import type { LLMModelItemType } from '@fastgpt/global/core/ai/model.d';
import { PaginationProps } from '@fastgpt/web/common/fetch/type';
/* ===== dataset ===== */
/* ======= collections =========== */
export type GetDatasetCollectionsProps = RequestPaging & {
export type GetDatasetCollectionsProps = PaginationProps<{
datasetId: string;
parentId?: string;
searchText?: string;
filterTags?: string[];
simple?: boolean;
selectFolder?: boolean;
};
}>;
/* ==== data ===== */

View File

@@ -55,7 +55,7 @@ function GroupInfoModal({ onClose, editGroupId }: { onClose: () => void; editGro
}
);
const { run: onCreate, loading: isLoadingCreate } = useRequest2(
const { runAsync: onCreate, loading: isLoadingCreate } = useRequest2(
(data: GroupFormType) => {
return postCreateGroup({
name: data.name,
@@ -67,7 +67,7 @@ function GroupInfoModal({ onClose, editGroupId }: { onClose: () => void; editGro
}
);
const { run: onUpdate, loading: isLoadingUpdate } = useRequest2(
const { runAsync: onUpdate, loading: isLoadingUpdate } = useRequest2(
async (data: GroupFormType) => {
if (!editGroupId) return;
return putUpdateGroup({

View File

@@ -32,26 +32,26 @@ export type GroupFormType = {
}[];
};
// 1. Owner can not be deleted, toast
// 2. Owner/Admin can manage members
// 3. Owner can add/remove admins
function GroupEditModal({ onClose, editGroupId }: { onClose: () => void; editGroupId?: string }) {
// 1. Owner can not be deleted, toast
// 2. Owner/Admin can manage members
// 3. Owner can add/remove admins
const { t } = useTranslation();
const { userInfo } = useUserStore();
const { toast } = useToast();
const [hoveredMemberId, setHoveredMemberId] = useState<string | undefined>(undefined);
const {
members: allMembers,
refetchGroups,
groups,
refetchMembers
} = useContextSelector(TeamContext, (v) => v);
const groups = useContextSelector(TeamContext, (v) => v.groups);
const refetchGroups = useContextSelector(TeamContext, (v) => v.refetchGroups);
const group = useMemo(() => {
return groups.find((item) => item._id === editGroupId);
}, [editGroupId, groups]);
const allMembers = useContextSelector(TeamContext, (v) => v.members);
const refetchMembers = useContextSelector(TeamContext, (v) => v.refetchMembers);
const MemberScrollData = useContextSelector(TeamContext, (v) => v.MemberScrollData);
const [hoveredMemberId, setHoveredMemberId] = useState<string>();
const [members, setMembers] = useState(group?.members || []);
const [searchKey, setSearchKey] = useState('');
const filtered = useMemo(() => {
return [
@@ -62,7 +62,7 @@ function GroupEditModal({ onClose, editGroupId }: { onClose: () => void; editGro
];
}, [searchKey, allMembers]);
const { run: onUpdate, loading: isLoadingUpdate } = useRequest2(
const { runAsync: onUpdate, loading: isLoadingUpdate } = useRequest2(
async () => {
if (!editGroupId || !members.length) return;
return putUpdateGroup({
@@ -155,7 +155,7 @@ function GroupEditModal({ onClose, editGroupId }: { onClose: () => void; editGro
setSearchKey(e.target.value);
}}
/>
<Flex flexDirection="column" mt={3} flexGrow="1" overflow={'auto'} maxH={'400px'}>
<MemberScrollData mt={3} flex={'1 0 0'} h={0}>
{filtered.map((member) => {
return (
<HStack
@@ -181,11 +181,11 @@ function GroupEditModal({ onClose, editGroupId }: { onClose: () => void; editGro
</HStack>
);
})}
</Flex>
</MemberScrollData>
</Flex>
<Flex borderLeft="1px" borderColor="myGray.200" flexDirection="column" p="4" h={'100%'}>
<Box mt={2}>{t('common:chosen') + ': ' + members.length}</Box>
<Flex mt={3} flexDirection="column" flexGrow="1" overflow={'auto'} maxH={'400px'}>
<MemberScrollData mt={3} flex={'1 0 0'} h={0}>
{members.map((member) => {
return (
<HStack
@@ -262,11 +262,14 @@ function GroupEditModal({ onClose, editGroupId }: { onClose: () => void; editGro
</HStack>
);
})}
</Flex>
</MemberScrollData>
</Flex>
</Grid>
</ModalBody>
<ModalFooter alignItems="flex-end">
<ModalFooter>
<Button variant={'whiteBase'} mr={3} onClick={onClose}>
{t('common:common.Close')}
</Button>
<Button isLoading={isLoading} onClick={onUpdate}>
{t('common:common.Save')}
</Button>

View File

@@ -24,50 +24,43 @@ import { useUserStore } from '@/web/support/user/useUserStore';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { deleteGroup } from '@/web/support/user/team/group/api';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import MemberTag from '../../../../../components/support/user/team/Info/MemberTag';
import MemberTag from '../../../../components/support/user/team/Info/MemberTag';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import dynamic from 'next/dynamic';
import { useState } from 'react';
import IconButton from '../OrgManage/IconButton';
import { MemberGroupType } from '@fastgpt/global/support/permission/memberGroup/type';
const ChangeOwnerModal = dynamic(() => import('./GroupTransferOwnerModal'));
const GroupInfoModal = dynamic(() => import('./GroupInfoModal'));
const ManageGroupMemberModal = dynamic(() => import('./GroupManageMember'));
const GroupManageMember = dynamic(() => import('./GroupManageMember'));
function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
const { t } = useTranslation();
const { userInfo } = useUserStore();
const [editGroupId, setEditGroupId] = useState<string>();
const { groups, refetchGroups, members, refetchMembers } = useContextSelector(
TeamContext,
(v) => v
);
const [editGroup, setEditGroup] = useState<MemberGroupType>();
const {
isOpen: isOpenGroupInfo,
onOpen: onOpenGroupInfo,
onClose: onCloseGroupInfo
} = useDisclosure();
const {
isOpen: isOpenManageGroupMember,
onOpen: onOpenManageGroupMember,
onClose: onCloseManageGroupMember
} = useDisclosure();
const onEditGroup = (groupId: string) => {
setEditGroupId(groupId);
const onEditGroupInfo = (e: MemberGroupType) => {
setEditGroup(e);
onOpenGroupInfo();
};
const onManageMember = (groupId: string) => {
setEditGroupId(groupId);
onOpenManageGroupMember();
};
const { ConfirmModal: ConfirmDeleteGroupModal, openConfirm: openDeleteGroupModal } = useConfirm({
type: 'delete',
content: t('account_team:confirm_delete_group')
});
const { groups, refetchGroups, members, refetchMembers } = useContextSelector(
TeamContext,
(v) => v
);
const { runAsync: delDeleteGroup } = useRequest2(deleteGroup, {
onSuccess: () => {
refetchGroups();
@@ -75,12 +68,21 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
}
});
const {
isOpen: isOpenManageGroupMember,
onOpen: onOpenManageGroupMember,
onClose: onCloseManageGroupMember
} = useDisclosure();
const onManageMember = (e: MemberGroupType) => {
setEditGroup(e);
onOpenManageGroupMember();
};
const hasGroupManagePer = (group: (typeof groups)[0]) =>
userInfo?.team.permission.hasManagePer ||
['admin', 'owner'].includes(
group.members.find((item) => item.tmbId === userInfo?.team.tmbId)?.role ?? ''
);
const isGroupOwner = (group: (typeof groups)[0]) =>
userInfo?.team.permission.hasManagePer ||
group.members.find((item) => item.role === 'owner')?.tmbId === userInfo?.team.tmbId;
@@ -90,8 +92,8 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
onOpen: onOpenChangeOwner,
onClose: onCloseChangeOwner
} = useDisclosure();
const onChangeOwner = (groupId: string) => {
setEditGroupId(groupId);
const onChangeOwner = (e: MemberGroupType) => {
setEditGroup(e);
onOpenChangeOwner();
};
@@ -173,7 +175,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
<AvatarGroup avatars={members.map((v) => v.avatar)} groupId={group._id} />
) : hasGroupManagePer(group) ? (
<MyTooltip label={t('account_team:manage_member')}>
<Box cursor="pointer" onClick={() => onManageMember(group._id)}>
<Box cursor="pointer" onClick={() => onManageMember(group)}>
<AvatarGroup
avatars={group.members.map(
(v) => members.find((m) => m.tmbId === v.tmbId)?.avatar ?? ''
@@ -202,14 +204,14 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
label: t('account_team:edit_info'),
icon: 'edit',
onClick: () => {
onEditGroup(group._id);
onEditGroupInfo(group);
}
},
{
label: t('account_team:manage_member'),
icon: 'support/team/group',
onClick: () => {
onManageMember(group._id);
onManageMember(group);
}
},
...(isGroupOwner(group)
@@ -218,7 +220,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
label: t('account_team:transfer_ownership'),
icon: 'modal/changePer',
onClick: () => {
onChangeOwner(group._id);
onChangeOwner(group);
},
type: 'primary' as MenuItemType
},
@@ -246,25 +248,25 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
</MyBox>
<ConfirmDeleteGroupModal />
{isOpenChangeOwner && editGroupId && (
<ChangeOwnerModal groupId={editGroupId} onClose={onCloseChangeOwner} />
{isOpenChangeOwner && editGroup && (
<ChangeOwnerModal groupId={editGroup._id} onClose={onCloseChangeOwner} />
)}
{isOpenGroupInfo && (
<GroupInfoModal
onClose={() => {
onCloseGroupInfo();
setEditGroupId(undefined);
setEditGroup(undefined);
}}
editGroupId={editGroupId}
editGroupId={editGroup?._id}
/>
)}
{isOpenManageGroupMember && (
<ManageGroupMemberModal
{isOpenManageGroupMember && editGroup && (
<GroupManageMember
onClose={() => {
onCloseManageGroupMember();
setEditGroupId(undefined);
setEditGroup(undefined);
}}
editGroupId={editGroupId}
editGroupId={editGroup._id}
/>
)}
</>

View File

@@ -13,7 +13,6 @@ import {
Tr,
useDisclosure
} from '@chakra-ui/react';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
import { useTranslation } from 'next-i18next';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
@@ -30,6 +29,9 @@ import { useToast } from '@fastgpt/web/hooks/useToast';
import { TeamErrEnum } from '@fastgpt/global/common/error/code/team';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { delLeaveTeam } from '@/web/support/user/team/api';
import { postSyncMembers } from '@/web/support/user/api';
import MyLoading from '@fastgpt/web/components/common/MyLoading';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
const InviteModal = dynamic(() => import('./InviteModal'));
const TeamTagModal = dynamic(() => import('@/components/support/user/team/TeamTagModal'));
@@ -40,8 +42,16 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
const { userInfo, teamPlanStatus } = useUserStore();
const { feConfigs, setNotSufficientModalType } = useSystemStore();
const { groups, refetchGroups, myTeams, refetchTeams, members, refetchMembers, onSwitchTeam } =
useContextSelector(TeamContext, (v) => v);
const {
groups,
refetchGroups,
myTeams,
refetchTeams,
members,
refetchMembers,
onSwitchTeam,
MemberScrollData
} = useContextSelector(TeamContext, (v) => v);
const {
isOpen: isOpenTeamTagsAsync,
@@ -54,6 +64,8 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
type: 'delete'
});
const isSyncMember = feConfigs.register_method?.includes('sync');
const { runAsync: onLeaveTeam } = useRequest2(
async () => {
const defaultTeam = myTeams.find((item) => item.defaultTeam) || myTeams[0];
@@ -72,8 +84,17 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
content: t('account_team:confirm_leave_team')
});
const { runAsync: onSyncMember, loading: isSyncing } = useRequest2(postSyncMembers, {
onSuccess() {
refetchMembers();
},
successToast: t('account_team:sync_member_success'),
errorToast: t('account_team:sync_member_failed')
});
return (
<>
{isSyncing && <MyLoading />}
<Flex justify={'space-between'} align={'center'} pb={'1rem'}>
{Tabs}
<HStack>
@@ -91,7 +112,21 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
{t('account_team:label_sync')}
</Button>
)}
{userInfo?.team.permission.hasManagePer && (
{userInfo?.team.permission.hasManagePer && isSyncMember && (
<Button
variant={'primary'}
size="md"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name="common/retryLight" w={'16px'} color={'white'} />}
onClick={() => {
onSyncMember();
}}
>
{t('account_team:sync_immediately')}
</Button>
)}
{userInfo?.team.permission.hasManagePer && !isSyncMember && (
<Button
variant={'primary'}
size="md"
@@ -135,76 +170,83 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
<Box flex={'1 0 0'} overflow={'auto'}>
<TableContainer overflow={'unset'} fontSize={'sm'}>
<Table overflow={'unset'}>
<Thead>
<Tr bgColor={'white !important'}>
<Th borderLeftRadius="6px" bgColor="myGray.100">
{t('account_team:user_name')}
</Th>
<Th bgColor="myGray.100">{t('account_team:member_group')}</Th>
<Th borderRightRadius="6px" bgColor="myGray.100">
{t('common:common.Action')}
</Th>
</Tr>
</Thead>
<Tbody>
{members?.map((item) => (
<Tr key={item.userId} overflow={'unset'}>
<Td>
<HStack>
<Avatar src={item.avatar} w={['18px', '22px']} borderRadius={'50%'} />
<Box className={'textEllipsis'}>
{item.memberName}
{item.status === 'waiting' && (
<Tag ml="2" colorSchema="yellow">
{t('account_team:waiting')}
</Tag>
)}
</Box>
</HStack>
</Td>
<Td maxW={'300px'}>
<GroupTags
names={groups
?.filter((group) => group.members.map((m) => m.tmbId).includes(item.tmbId))
.map((g) => g.name)}
max={3}
/>
</Td>
<Td>
{userInfo?.team.permission.hasManagePer &&
item.role !== TeamMemberRoleEnum.owner &&
item.tmbId !== userInfo?.team.tmbId && (
<Icon
name={'common/trash'}
cursor={'pointer'}
w="1rem"
p="1"
borderRadius="sm"
_hover={{
color: 'red.600',
bgColor: 'myGray.100'
}}
onClick={() => {
openRemoveMember(
() =>
delRemoveMember(item.tmbId).then(() =>
Promise.all([refetchGroups(), refetchMembers()])
),
undefined,
t('account_team:remove_tip', {
username: item.memberName
})
)();
}}
/>
)}
</Td>
<MemberScrollData>
<Table overflow={'unset'}>
<Thead>
<Tr bgColor={'white !important'}>
<Th borderLeftRadius="6px" bgColor="myGray.100">
{t('account_team:user_name')}
</Th>
<Th bgColor="myGray.100">{t('account_team:member_group')}</Th>
{!isSyncMember && (
<Th borderRightRadius="6px" bgColor="myGray.100">
{t('common:common.Action')}
</Th>
)}
</Tr>
))}
</Tbody>
</Table>
</Thead>
<Tbody>
{members?.map((item) => (
<Tr key={item.userId} overflow={'unset'}>
<Td>
<HStack>
<Avatar src={item.avatar} w={['18px', '22px']} borderRadius={'50%'} />
<Box className={'textEllipsis'}>
{item.memberName}
{item.status === 'waiting' && (
<Tag ml="2" colorSchema="yellow">
{t('account_team:waiting')}
</Tag>
)}
</Box>
</HStack>
</Td>
<Td maxW={'300px'}>
<GroupTags
names={groups
?.filter((group) =>
group.members.map((m) => m.tmbId).includes(item.tmbId)
)
.map((g) => g.name)}
max={3}
/>
</Td>
{!isSyncMember && (
<Td>
{userInfo?.team.permission.hasManagePer &&
item.role !== TeamMemberRoleEnum.owner &&
item.tmbId !== userInfo?.team.tmbId && (
<Icon
name={'common/trash'}
cursor={'pointer'}
w="1rem"
p="1"
borderRadius="sm"
_hover={{
color: 'red.600',
bgColor: 'myGray.100'
}}
onClick={() => {
openRemoveMember(
() =>
delRemoveMember(item.tmbId).then(() =>
Promise.all([refetchGroups(), refetchMembers()])
),
undefined,
t('account_team:remove_tip', {
username: item.memberName
})
)();
}}
/>
)}
</Td>
)}
</Tr>
))}
</Tbody>
</Table>
</MemberScrollData>
<ConfirmRemoveMemberModal />
</TableContainer>
</Box>

View File

@@ -22,7 +22,6 @@ import { useMemo, useState } from 'react';
import { useContextSelector } from 'use-context-selector';
import { TeamContext } from '../context';
import { OrgType } from '@fastgpt/global/support/user/team/org/type';
import dynamic from 'next/dynamic';
export type GroupFormType = {
members: {
@@ -51,7 +50,7 @@ function OrgMemberManageModal({
onClose: () => void;
}) {
const { t } = useTranslation();
const allMembers = useContextSelector(TeamContext, (v) => v.members);
const { members: allMembers, MemberScrollData } = useContextSelector(TeamContext, (v) => v);
const [selectedMembers, setSelectedMembers] = useState<string[]>(
currentOrg.members.map((item) => item.tmbId)
@@ -98,7 +97,6 @@ function OrgMemberManageModal({
return (
<MyModal
onClose={onClose}
isOpen
title={t('user:team.group.manage_member')}
iconSrc={currentOrg?.avatar}
@@ -124,32 +122,34 @@ function OrgMemberManageModal({
}}
/>
<Flex flexDirection="column" mt={3} flexGrow="1" overflow={'auto'} maxH={'400px'}>
{filterMembers.map((member) => {
return (
<HStack
py="2"
px={3}
borderRadius={'md'}
alignItems="center"
key={member.tmbId}
cursor={'pointer'}
_hover={{
bg: 'myGray.50',
...(!isSelected(member.tmbId) ? { svg: { color: 'myGray.50' } } : {})
}}
_notLast={{ mb: 2 }}
onClick={() => handleToggleSelect(member.tmbId)}
>
<Checkbox
isChecked={!!isSelected(member.tmbId)}
icon={<CheckboxIcon name={'common/check'} />}
pointerEvents="none"
/>
<Avatar src={member.avatar} w="1.5rem" borderRadius={'50%'} />
<Box>{member.memberName}</Box>
</HStack>
);
})}
<MemberScrollData>
{filterMembers.map((member) => {
return (
<HStack
py="2"
px={3}
borderRadius={'md'}
alignItems="center"
key={member.tmbId}
cursor={'pointer'}
_hover={{
bg: 'myGray.50',
...(!isSelected(member.tmbId) ? { svg: { color: 'myGray.50' } } : {})
}}
_notLast={{ mb: 2 }}
onClick={() => handleToggleSelect(member.tmbId)}
>
<Checkbox
isChecked={!!isSelected(member.tmbId)}
icon={<CheckboxIcon name={'common/check'} />}
pointerEvents="none"
/>
<Avatar src={member.avatar} w="1.5rem" borderRadius={'50%'} />
<Box>{member.memberName}</Box>
</HStack>
);
})}
</MemberScrollData>
</Flex>
</Flex>
<Flex borderLeft="1px" borderColor="myGray.200" flexDirection="column" p="4" h={'100%'}>
@@ -185,7 +185,10 @@ function OrgMemberManageModal({
</Flex>
</Grid>
</ModalBody>
<ModalFooter alignItems="flex-end">
<ModalFooter>
<Button variant={'whiteBase'} mr={3} onClick={onClose}>
{t('common:common.Close')}
</Button>
<Button isLoading={isLoading} onClick={onUpdate}>
{t('common:common.Save')}
</Button>

View File

@@ -0,0 +1,369 @@
import { useUserStore } from '@/web/support/user/useUserStore';
import {
Box,
Divider,
Flex,
HStack,
Table,
TableContainer,
Tag,
Tbody,
Td,
Th,
Thead,
Tr,
VStack
} from '@chakra-ui/react';
import type { OrgType } from '@fastgpt/global/support/user/team/org/type';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import type { IconNameType } from '@fastgpt/web/components/common/Icon/type';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import { useMemo, useState } from 'react';
import { useContextSelector } from 'use-context-selector';
import MemberTag from '@/components/support/user/team/Info/MemberTag';
import { TeamContext } from '../context';
import { deleteOrg, deleteOrgMember, getOrgList } from '@/web/support/user/team/org/api';
import IconButton from './IconButton';
import { defaultOrgForm, type OrgFormType } from './OrgInfoModal';
import dynamic from 'next/dynamic';
import MyBox from '@fastgpt/web/components/common/MyBox';
import Path from '@/components/common/folder/Path';
import { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type';
import { getOrgChildrenPath } from '@fastgpt/global/support/user/team/org/constant';
import { useSystemStore } from '@/web/common/system/useSystemStore';
const OrgInfoModal = dynamic(() => import('./OrgInfoModal'));
const OrgMemberManageModal = dynamic(() => import('./OrgMemberManageModal'));
const OrgMoveModal = dynamic(() => import('./OrgMoveModal'));
function ActionButton({
icon,
text,
onClick
}: {
icon: IconNameType;
text: string;
onClick: () => void;
}) {
return (
<HStack
gap={'8px'}
w="100%"
transition={'background 0.1s'}
cursor={'pointer'}
p="4px"
rounded={'sm'}
_hover={{
bg: 'myGray.05',
color: 'primary.600'
}}
onClick={onClick}
>
<MyIcon name={icon} w="1rem" h="1rem" />
<Box fontSize={'sm'}>{text}</Box>
</HStack>
);
}
function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
const { t } = useTranslation();
const { userInfo, isTeamAdmin } = useUserStore();
const { members, MemberScrollData } = useContextSelector(TeamContext, (v) => v);
const { feConfigs } = useSystemStore();
const isSyncMember = feConfigs.register_method?.includes('sync');
const [parentPath, setParentPath] = useState('');
const {
data: orgs = [],
loading: isLoadingOrgs,
refresh: refetchOrgs
} = useRequest2(getOrgList, {
manual: false,
refreshDeps: [userInfo?.team?.teamId]
});
const currentOrgs = useMemo(() => {
if (orgs.length === 0) return [];
// Auto select the first org(root org is team)
if (parentPath === '') {
setParentPath(getOrgChildrenPath(orgs[0]));
return [];
}
return orgs
.filter((org) => org.path === parentPath)
.map((item) => {
return {
...item,
// Member + org
count:
item.members.length + orgs.filter((org) => org.path === getOrgChildrenPath(item)).length
};
});
}, [orgs, parentPath]);
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 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 [editOrg, setEditOrg] = useState<OrgFormType>();
const [manageMemberOrg, setManageMemberOrg] = useState<OrgType>();
const [movingOrg, setMovingOrg] = useState<OrgType>();
// Delete org
const { ConfirmModal: ConfirmDeleteOrgModal, openConfirm: openDeleteOrgModal } = useConfirm({
type: 'delete',
content: t('account_team:confirm_delete_org')
});
const deleteOrgHandler = (orgId: string) => openDeleteOrgModal(() => deleteOrgReq(orgId))();
const { runAsync: deleteOrgReq } = useRequest2(deleteOrg, {
onSuccess: () => {
refetchOrgs();
}
});
// Delete member
const { ConfirmModal: ConfirmDeleteMember, openConfirm: openDeleteMemberModal } = useConfirm({
type: 'delete',
content: t('account_team:confirm_delete_member')
});
const { runAsync: deleteMemberReq } = useRequest2(deleteOrgMember, {
onSuccess: () => {
refetchOrgs();
}
});
return (
<>
<Flex justify={'space-between'} align={'center'} pb={'1rem'}>
{Tabs}
</Flex>
<MyBox
flex={'1 0 0'}
h={0}
display={'flex'}
flexDirection={'column'}
isLoading={isLoadingOrgs}
>
<Box mb={3}>
<Path paths={paths} rootName={userInfo?.team?.teamName} onClick={setParentPath} />
</Box>
<Flex flex={'1 0 0'} h={0} w={'100%'} gap={'4'}>
<MemberScrollData h={'100%'} fontSize={'sm'} flexGrow={1}>
{/* Table */}
<TableContainer>
<Table>
<Thead>
<Tr bg={'white !important'}>
<Th bg="myGray.100" borderLeftRadius="6px">
{t('common:Name')}
</Th>
{!isSyncMember && (
<Th bg="myGray.100" borderRightRadius="6px">
{t('common:common.Action')}
</Th>
)}
</Tr>
</Thead>
<Tbody>
{currentOrgs.map((org) => (
<Tr key={org._id} overflow={'unset'}>
<Td>
<HStack
cursor={'pointer'}
onClick={() => setParentPath(getOrgChildrenPath(org))}
>
<MemberTag name={org.name} avatar={org.avatar} />
<Tag size="sm">{org.count}</Tag>
<MyIcon
name="core/chat/chevronRight"
w={'1rem'}
h={'1rem'}
color={'myGray.500'}
/>
</HStack>
</Td>
{isTeamAdmin && !isSyncMember && (
<Td w={'6rem'}>
<MyMenu
trigger="hover"
Button={<IconButton name="more" />}
menuList={[
{
children: [
{
icon: 'edit',
label: t('account_team:edit_info'),
onClick: () => setEditOrg(org)
},
{
icon: 'common/file/move',
label: t('common:Move'),
onClick: () => setMovingOrg(org)
},
{
icon: 'delete',
label: t('account_team:delete'),
type: 'danger',
onClick: () => deleteOrgHandler(org._id)
}
]
}
]}
/>
</Td>
)}
</Tr>
))}
{currentOrg?.members.map((member) => {
const memberInfo = members.find((m) => m.tmbId === member.tmbId);
if (!memberInfo) return null;
return (
<Tr key={member.tmbId}>
<Td>
<MemberTag name={memberInfo.memberName} avatar={memberInfo.avatar} />
</Td>
<Td w={'6rem'}>
{isTeamAdmin && !isSyncMember && (
<MyMenu
trigger={'hover'}
Button={<IconButton name="more" />}
menuList={[
{
children: [
{
icon: 'delete',
label: t('account_team:delete'),
type: 'danger',
onClick: () =>
openDeleteMemberModal(() =>
deleteMemberReq(currentOrg._id, member.tmbId)
)()
}
]
}
]}
/>
)}
</Td>
</Tr>
);
})}
</Tbody>
</Table>
</TableContainer>
</MemberScrollData>
{/* Slider */}
{!isSyncMember && (
<VStack w={'180px'} alignItems={'start'}>
<HStack gap={'6px'}>
<Avatar src={currentOrg?.avatar} w={'1rem'} h={'1rem'} rounded={'xs'} />
<Box fontWeight={500} color={'myGray.900'}>
{currentOrg?.name}
</Box>
{currentOrg?.path !== '' && (
<IconButton name="edit" onClick={() => setEditOrg(currentOrg)} />
)}
</HStack>
<Box fontSize={'xs'}>{currentOrg?.description || t('common:common.no_intro')}</Box>
<Divider my={'20px'} />
<Box fontWeight={500} fontSize="sm" color="myGray.900">
{t('common:common.Action')}
</Box>
{currentOrg && isTeamAdmin && (
<VStack gap="13px" w="100%">
<ActionButton
icon="common/add2"
text={t('account_team:create_sub_org')}
onClick={() => {
setEditOrg({
...defaultOrgForm,
parentId: currentOrg?._id
});
}}
/>
<ActionButton
icon="common/administrator"
text={t('account_team:manage_member')}
onClick={() => setManageMemberOrg(currentOrg)}
/>
{currentOrg?.path !== '' && (
<>
<ActionButton
icon="common/file/move"
text={t('account_team:move_org')}
onClick={() => setMovingOrg(currentOrg)}
/>
<ActionButton
icon="delete"
text={t('account_team:delete_org')}
onClick={() => deleteOrgHandler(currentOrg._id)}
/>
</>
)}
</VStack>
)}
</VStack>
)}
</Flex>
</MyBox>
{!!editOrg && (
<OrgInfoModal
editOrg={editOrg}
onClose={() => setEditOrg(undefined)}
onSuccess={refetchOrgs}
/>
)}
{!!movingOrg && (
<OrgMoveModal
orgs={orgs}
movingOrg={movingOrg}
onClose={() => setMovingOrg(undefined)}
onSuccess={refetchOrgs}
/>
)}
{!!manageMemberOrg && (
<OrgMemberManageModal
currentOrg={manageMemberOrg}
refetchOrgs={refetchOrgs}
onClose={() => setManageMemberOrg(undefined)}
/>
)}
<ConfirmDeleteOrgModal />
<ConfirmDeleteMember />
</>
);
}
export default OrgTable;

View File

@@ -24,7 +24,7 @@ import {
import { useUserStore } from '@/web/support/user/useUserStore';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MemberTag from '../../../../../components/support/user/team/Info/MemberTag';
import MemberTag from '../../../../components/support/user/team/Info/MemberTag';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import {
TeamManagePermissionVal,

View File

@@ -2,7 +2,7 @@ import React, { ReactNode, useState } from 'react';
import { createContext } from 'use-context-selector';
import type { EditTeamFormDataType } from './EditInfoModal';
import dynamic from 'next/dynamic';
import { getTeamList, putSwitchTeam } from '@/web/support/user/team/api';
import { getTeamList, getTeamMembers, putSwitchTeam } from '@/web/support/user/team/api';
import { TeamMemberStatusEnum } from '@fastgpt/global/support/user/team/constant';
import { useUserStore } from '@/web/support/user/useUserStore';
import type { TeamTmbItemType, TeamMemberItemType } from '@fastgpt/global/support/user/team/type';
@@ -10,7 +10,7 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import { getGroupList } from '@/web/support/user/team/group/api';
import { MemberGroupListType } from '@fastgpt/global/support/permission/memberGroup/type';
import { OrgType } from '@fastgpt/global/support/user/team/org/type';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
const EditInfoModal = dynamic(() => import('./EditInfoModal'));
@@ -26,6 +26,7 @@ type TeamModalContextType = {
refetchTeams: () => void;
refetchGroups: () => void;
teamSize: number;
MemberScrollData: ReturnType<typeof useScrollPagination>['ScrollData'];
};
export const TeamContext = createContext<TeamModalContextType>({
@@ -49,13 +50,14 @@ export const TeamContext = createContext<TeamModalContextType>({
throw new Error('Function not implemented.');
},
teamSize: 0
teamSize: 0,
MemberScrollData: () => <></>
});
export const TeamModalContextProvider = ({ children }: { children: ReactNode }) => {
const { t } = useTranslation();
const [editTeamData, setEditTeamData] = useState<EditTeamFormDataType>();
const { userInfo, initUserInfo, loadAndGetTeamMembers } = useUserStore();
const { userInfo, initUserInfo } = useUserStore();
const {
data: myTeams = [],
@@ -69,18 +71,11 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode })
// member action
const {
data: members = [],
runAsync: refetchMembers,
loading: loadingMembers
} = useRequest2(
() => {
if (!userInfo?.team?.teamId) return Promise.resolve([]);
return loadAndGetTeamMembers(true);
},
{
manual: false,
refreshDeps: [userInfo?.team?.teamId]
}
);
isLoading: loadingMembers,
refreshList: refetchMembers,
total: memberTotal,
ScrollData: MemberScrollData
} = useScrollPagination(getTeamMembers, {});
const { runAsync: onSwitchTeam, loading: isSwitchingTeam } = useRequest2(
async (teamId: string) => {
@@ -115,7 +110,8 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode })
refetchMembers,
groups,
refetchGroups,
teamSize: members.length
teamSize: memberTotal,
MemberScrollData
};
return (

View File

@@ -1,4 +1,4 @@
import React, { useState, useCallback, useMemo, useEffect } from 'react';
import React, { useState, useMemo, useEffect } from 'react';
import {
Button,
Table,
@@ -25,7 +25,6 @@ import {
billStatusMap,
billTypeMap
} from '@fastgpt/global/support/wallet/bill/constants';
// import { usePagination } from '@/web/common/hooks/usePagination';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { standardSubLevelMap, subModeMap } from '@fastgpt/global/support/wallet/sub/constants';
@@ -33,25 +32,23 @@ import MySelect from '@fastgpt/web/components/common/MySelect';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { usePagination } from '@fastgpt/web/hooks/usePagination';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import { useI18n } from '@/web/context/I18n';
const BillTable = () => {
const { t } = useTranslation();
const { commonT } = useI18n();
const { toast } = useToast();
const [billType, setBillType] = useState<BillTypeEnum | ''>('');
const [billType, setBillType] = useState<BillTypeEnum | undefined>(undefined);
const [billDetail, setBillDetail] = useState<BillSchemaType>();
const billTypeList = useMemo(
() =>
[
{ label: t('account_bill:all'), value: '' },
{ label: t('account_bill:all'), value: undefined },
...Object.entries(billTypeMap).map(([key, value]) => ({
label: t(value.label as any),
value: key
}))
] as {
label: string;
value: BillTypeEnum | '';
value: BillTypeEnum | undefined;
}[],
[t]
);
@@ -62,8 +59,7 @@ const BillTable = () => {
Pagination,
getData,
total
} = usePagination({
api: getBills,
} = usePagination(getBills, {
pageSize: 20,
params: {
type: billType
@@ -110,7 +106,7 @@ const BillTable = () => {
<Tr>
<Th>#</Th>
<Th>
<MySelect<BillTypeEnum | ''>
<MySelect
list={billTypeList}
value={billType}
size={'sm'}
@@ -181,7 +177,6 @@ export default BillTable;
function BillDetailModal({ bill, onClose }: { bill: BillSchemaType; onClose: () => void }) {
const { t } = useTranslation();
const { commonT } = useI18n();
return (
<MyModal
isOpen={true}

View File

@@ -1,7 +1,7 @@
import { getInvoiceRecords } from '@/web/support/wallet/bill/invoice/api';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { useTranslation } from 'next-i18next';
import { useEffect, useState } from 'react';
import { useState } from 'react';
import {
Box,
Button,
@@ -30,8 +30,7 @@ const InvoiceTable = () => {
isLoading,
Pagination,
total
} = usePagination({
api: getInvoiceRecords,
} = usePagination(getInvoiceRecords, {
pageSize: 20
});

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useMemo, useRef } from 'react';
import React, { useCallback, useMemo } from 'react';
import {
Box,
Flex,
@@ -160,6 +160,7 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => {
color: 'myGray.900'
};
const isSyncMember = feConfigs.register_method?.includes('sync');
return (
<Box>
{/* user info */}
@@ -224,6 +225,7 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => {
<Box {...labelStyles}>{t('account_info:member_name')}:&nbsp;</Box>
<Input
flex={'1 0 0'}
disabled={isSyncMember}
defaultValue={userInfo?.team?.memberName || 'Member'}
title={t('account_info:click_modify_nickname')}
borderColor={'transparent'}
@@ -590,11 +592,6 @@ const Other = ({ onOpenContact }: { onOpenContact: () => void }) => {
const { feConfigs } = useSystemStore();
const { t } = useTranslation();
const { isPc } = useSystem();
const { userInfo, updateUserInfo } = useUserStore();
const { reset } = useForm<UserUpdateParams>({
defaultValues: userInfo as UserType
});
return (
<Box>

View File

@@ -1,13 +1,12 @@
import React from 'react';
import { Box, Button, Flex, useTheme } from '@chakra-ui/react';
import { getInforms, readInform } from '@/web/support/user/inform/api';
import type { UserInformSchema } from '@fastgpt/global/support/user/inform/type';
import { formatTimeToChatTime } from '@fastgpt/global/common/string/time';
import { usePagination } from '@fastgpt/web/hooks/usePagination';
import { useLoading } from '@fastgpt/web/hooks/useLoading';
import { useTranslation } from 'next-i18next';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import AccountContainer, { TabEnum } from './components/AccountContainer';
import AccountContainer from './components/AccountContainer';
import { serviceSideProps } from '@fastgpt/web/common/system/nextjs';
const InformTable = () => {
@@ -23,8 +22,7 @@ const InformTable = () => {
Pagination,
getData,
pageNum
} = usePagination<UserInformSchema>({
api: getInforms,
} = usePagination(getInforms, {
pageSize: 20
});

View File

@@ -25,7 +25,7 @@ import { usePagination } from '@fastgpt/web/hooks/usePagination';
import { useLoading } from '@fastgpt/web/hooks/useLoading';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import AccountContainer, { TabEnum } from './components/AccountContainer';
import AccountContainer from './components/AccountContainer';
import { serviceSideProps } from '@fastgpt/web/common/system/nextjs';
const Promotion = () => {
@@ -41,8 +41,7 @@ const Promotion = () => {
total,
pageSize,
Pagination
} = usePagination({
api: getPromotionRecords,
} = usePagination(getPromotionRecords, {
pageSize: 20
});

View File

@@ -1,354 +0,0 @@
import { deleteOrg, deleteOrgMember } from '@/web/support/user/team/org/api';
import { useUserStore } from '@/web/support/user/useUserStore';
import {
Box,
Divider,
Flex,
HStack,
Table,
TableContainer,
Tag,
Tbody,
Td,
Th,
Thead,
Tr,
VStack
} from '@chakra-ui/react';
import type { OrgType } from '@fastgpt/global/support/user/team/org/type';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import type { IconNameType } from '@fastgpt/web/components/common/Icon/type';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import { useMemo, useState } from 'react';
import { useContextSelector } from 'use-context-selector';
import MemberTag from '@/components/support/user/team/Info/MemberTag';
import { TeamContext } from '../context';
import { getOrgList } from '@/web/support/user/team/org/api';
import IconButton from './IconButton';
import { defaultOrgForm, type OrgFormType } from './OrgInfoModal';
import dynamic from 'next/dynamic';
import MyBox from '@fastgpt/web/components/common/MyBox';
import Path from '@/components/common/folder/Path';
import { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type';
import { getOrgChildrenPath } from '@fastgpt/global/support/user/team/org/constant';
const OrgInfoModal = dynamic(() => import('./OrgInfoModal'));
const OrgMemberManageModal = dynamic(() => import('./OrgMemberManageModal'));
const OrgMoveModal = dynamic(() => import('./OrgMoveModal'));
function ActionButton({
icon,
text,
onClick
}: {
icon: IconNameType;
text: string;
onClick: () => void;
}) {
return (
<HStack
gap={'8px'}
w="100%"
transition={'background 0.1s'}
cursor={'pointer'}
p="4px"
rounded={'sm'}
_hover={{
bg: 'myGray.05',
color: 'primary.600'
}}
onClick={onClick}
>
<MyIcon name={icon} w="1rem" h="1rem" />
<Box fontSize={'sm'}>{text}</Box>
</HStack>
);
}
function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
const { t } = useTranslation();
const { userInfo, isTeamAdmin } = useUserStore();
const { members } = useContextSelector(TeamContext, (v) => v);
const [parentPath, setParentPath] = useState('');
const {
data: orgs = [],
loading: isLoadingOrgs,
refresh: refetchOrgs
} = useRequest2(getOrgList, {
manual: false,
refreshDeps: [userInfo?.team?.teamId]
});
const currentOrgs = useMemo(() => {
if (orgs.length === 0) return [];
// Auto select the first org(root org is team)
if (parentPath === '') {
setParentPath(getOrgChildrenPath(orgs[0]));
return [];
}
return orgs
.filter((org) => org.path === parentPath)
.map((item) => {
return {
...item,
// Member + org
count:
item.members.length + orgs.filter((org) => org.path === getOrgChildrenPath(item)).length
};
});
}, [orgs, parentPath]);
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 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 [editOrg, setEditOrg] = useState<OrgFormType>();
const [manageMemberOrg, setManageMemberOrg] = useState<OrgType>();
const [movingOrg, setMovingOrg] = useState<OrgType>();
// Delete org
const { ConfirmModal: ConfirmDeleteOrgModal, openConfirm: openDeleteOrgModal } = useConfirm({
type: 'delete',
content: t('account_team:confirm_delete_org')
});
const deleteOrgHandler = (orgId: string) => openDeleteOrgModal(() => deleteOrgReq(orgId))();
const { runAsync: deleteOrgReq } = useRequest2(deleteOrg, {
onSuccess: () => {
refetchOrgs();
}
});
// Delete member
const { ConfirmModal: ConfirmDeleteMember, openConfirm: openDeleteMemberModal } = useConfirm({
type: 'delete',
content: t('account_team:confirm_delete_member')
});
const { runAsync: deleteMemberReq } = useRequest2(deleteOrgMember, {
onSuccess: () => {
refetchOrgs();
}
});
return (
<>
<Flex justify={'space-between'} align={'center'} pb={'1rem'}>
{Tabs}
</Flex>
<MyBox flex={'1 0 0'} overflow={'auto'} isLoading={isLoadingOrgs}>
<Box mb={3}>
<Path paths={paths} rootName={userInfo?.team?.teamName} onClick={setParentPath} />
</Box>
<Flex w={'100%'} gap={'4'}>
{/* Table */}
<TableContainer overflow={'unset'} fontSize={'sm'} flexGrow={1}>
<Table overflow={'unset'}>
<Thead>
<Tr bg={'white !important'}>
<Th bg="myGray.100" borderLeftRadius="6px">
{t('common:Name')}
</Th>
<Th bg="myGray.100" borderRightRadius="6px">
{t('common:common.Action')}
</Th>
</Tr>
</Thead>
<Tbody>
{currentOrgs.map((org) => (
<Tr key={org._id} overflow={'unset'}>
<Td>
<HStack
cursor={'pointer'}
onClick={() => setParentPath(getOrgChildrenPath(org))}
>
<MemberTag name={org.name} avatar={org.avatar} />
<Tag size="sm">{org.count}</Tag>
<MyIcon
name="core/chat/chevronRight"
w={'1rem'}
h={'1rem'}
color={'myGray.500'}
/>
</HStack>
</Td>
<Td w={'6rem'}>
{isTeamAdmin && (
<MyMenu
trigger="hover"
Button={<IconButton name="more" />}
menuList={[
{
children: [
{
icon: 'edit',
label: t('account_team:edit_info'),
onClick: () => setEditOrg(org)
},
{
icon: 'common/file/move',
label: t('common:Move'),
onClick: () => setMovingOrg(org)
},
{
icon: 'delete',
label: t('account_team:delete'),
type: 'danger',
onClick: () => deleteOrgHandler(org._id)
}
]
}
]}
/>
)}
</Td>
</Tr>
))}
{currentOrg?.members.map((member) => {
const memberInfo = members.find((m) => m.tmbId === member.tmbId);
if (!memberInfo) return null;
return (
<Tr key={member.tmbId}>
<Td>
<MemberTag name={memberInfo.memberName} avatar={memberInfo.avatar} />
</Td>
<Td w={'6rem'}>
{isTeamAdmin && (
<MyMenu
trigger={'hover'}
Button={<IconButton name="more" />}
menuList={[
{
children: [
{
icon: 'delete',
label: t('account_team:delete'),
type: 'danger',
onClick: () =>
openDeleteMemberModal(() =>
deleteMemberReq(currentOrg._id, member.tmbId)
)()
}
]
}
]}
/>
)}
</Td>
</Tr>
);
})}
</Tbody>
</Table>
</TableContainer>
{/* Slider */}
<VStack w={'180px'} alignItems={'start'}>
<HStack gap={'6px'}>
<Avatar src={currentOrg?.avatar} w={'1rem'} h={'1rem'} rounded={'xs'} />
<Box fontWeight={500} color={'myGray.900'}>
{currentOrg?.name}
</Box>
{currentOrg?.path !== '' && (
<IconButton name="edit" onClick={() => setEditOrg(currentOrg)} />
)}
</HStack>
<Box fontSize={'xs'}>{currentOrg?.description || t('common:common.no_intro')}</Box>
<Divider my={'20px'} />
<Box fontWeight={500} fontSize="sm" color="myGray.900">
{t('common:common.Action')}
</Box>
{currentOrg && isTeamAdmin && (
<VStack gap="13px" w="100%">
<ActionButton
icon="common/add2"
text={t('account_team:create_sub_org')}
onClick={() => {
setEditOrg({
...defaultOrgForm,
parentId: currentOrg?._id
});
}}
/>
<ActionButton
icon="common/administrator"
text={t('account_team:manage_member')}
onClick={() => setManageMemberOrg(currentOrg)}
/>
{currentOrg?.path !== '' && (
<>
<ActionButton
icon="common/file/move"
text={t('account_team:move_org')}
onClick={() => setMovingOrg(currentOrg)}
/>
<ActionButton
icon="delete"
text={t('account_team:delete_org')}
onClick={() => deleteOrgHandler(currentOrg._id)}
/>
</>
)}
</VStack>
)}
</VStack>
</Flex>
{!!editOrg && (
<OrgInfoModal
editOrg={editOrg}
onClose={() => setEditOrg(undefined)}
onSuccess={refetchOrgs}
/>
)}
{!!movingOrg && (
<OrgMoveModal
orgs={orgs}
movingOrg={movingOrg}
onClose={() => setMovingOrg(undefined)}
onSuccess={refetchOrgs}
/>
)}
{!!manageMemberOrg && (
<OrgMemberManageModal
currentOrg={manageMemberOrg}
refetchOrgs={refetchOrgs}
onClose={() => setManageMemberOrg(undefined)}
/>
)}
<ConfirmDeleteOrgModal />
<ConfirmDeleteMember />
</MyBox>
</>
);
}
export default OrgTable;

View File

@@ -1,6 +1,6 @@
import { serviceSideProps } from '@fastgpt/web/common/system/nextjs';
import AccountContainer from '../components/AccountContainer';
import { Box, Flex, useDisclosure } from '@chakra-ui/react';
import { Box, Flex } from '@chakra-ui/react';
import Icon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
import TeamSelector from '../components/TeamSelector';
@@ -11,14 +11,15 @@ import { useRouter } from 'next/router';
import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
import { TeamContext, TeamModalContextProvider } from './components/context';
import { TeamContext, TeamModalContextProvider } from '@/pageComponents/account/team/context';
import dynamic from 'next/dynamic';
import MemberTable from './components/MemberTable';
const PermissionManage = dynamic(() => import('./components/PermissionManage/index'));
const GroupManage = dynamic(() => import('./components/GroupManage/index'));
const OrgManage = dynamic(() => import('./components/OrgManage/index'));
const MemberTable = dynamic(() => import('@/pageComponents/account/team/MemberTable'));
const PermissionManage = dynamic(
() => import('@/pageComponents/account/team/PermissionManage/index')
);
const GroupManage = dynamic(() => import('@/pageComponents/account/team/GroupManage/index'));
const OrgManage = dynamic(() => import('@/pageComponents/account/team/OrgManage/index'));
export enum TeamTabEnum {
member = 'member',
@@ -34,7 +35,7 @@ const Team = () => {
const { t } = useTranslation();
const { userInfo } = useUserStore();
const { setEditTeamData, teamSize, isLoading } = useContextSelector(TeamContext, (v) => v);
const { setEditTeamData, isLoading, teamSize } = useContextSelector(TeamContext, (v) => v);
const Tabs = useMemo(
() => (
@@ -62,72 +63,81 @@ const Team = () => {
return (
<AccountContainer isLoading={isLoading}>
{/* header */}
<Flex
w={'100%'}
h={'3.5rem'}
px={'1.56rem'}
py={'0.56rem'}
borderBottom={'1px solid'}
borderColor={'myGray.200'}
bg={'myGray.25'}
align={'center'}
gap={6}
justify={'space-between'}
>
<Flex align={'center'}>
<Flex gap={2} color={'myGray.900'}>
<Icon name="support/user/usersLight" w={'1.25rem'} h={'1.25rem'} />
<Box fontWeight={'500'} fontSize={'1rem'}>
{t('account:team')}
</Box>
</Flex>
<Flex align={'center'} ml={6}>
<TeamSelector height={'28px'} />
</Flex>
{userInfo?.team?.role === TeamMemberRoleEnum.owner && (
<Flex align={'center'} justify={'center'} ml={2} p={'0.44rem'}>
<MyIcon
name="edit"
w="18px"
cursor="pointer"
_hover={{
color: 'primary.500'
}}
onClick={() => {
if (!userInfo?.team) return;
setEditTeamData({
id: userInfo.team.teamId,
name: userInfo.team.teamName,
avatar: userInfo.team.avatar
});
}}
/>
<Flex h={'100%'} flexDirection={'column'}>
{/* header */}
<Flex
w={'100%'}
h={'3.5rem'}
px={'1.56rem'}
py={'0.56rem'}
borderBottom={'1px solid'}
borderColor={'myGray.200'}
bg={'myGray.25'}
align={'center'}
gap={6}
justify={'space-between'}
>
<Flex align={'center'}>
<Flex gap={2} color={'myGray.900'}>
<Icon name="support/user/usersLight" w={'1.25rem'} h={'1.25rem'} />
<Box fontWeight={'500'} fontSize={'1rem'}>
{t('account:team')}
</Box>
</Flex>
)}
<Flex align={'center'} ml={6}>
<TeamSelector height={'28px'} />
</Flex>
{userInfo?.team?.role === TeamMemberRoleEnum.owner && (
<Flex align={'center'} justify={'center'} ml={2} p={'0.44rem'}>
<MyIcon
name="edit"
w="18px"
cursor="pointer"
_hover={{
color: 'primary.500'
}}
onClick={() => {
if (!userInfo?.team) return;
setEditTeamData({
id: userInfo.team.teamId,
name: userInfo.team.teamName,
avatar: userInfo.team.avatar
});
}}
/>
</Flex>
)}
</Flex>
<Box
float={'right'}
color={'myGray.900'}
h={'1.25rem'}
px={'0.5rem'}
py={'0.125rem'}
fontSize={'0.75rem'}
borderRadius={'1.25rem'}
bg={'myGray.150'}
>
{t('account_team:total_team_members', { amount: teamSize })}
</Box>
</Flex>
{/* table */}
<Box
float={'right'}
color={'myGray.900'}
h={'1.25rem'}
px={'0.5rem'}
py={'0.125rem'}
fontSize={'0.75rem'}
borderRadius={'1.25rem'}
bg={'myGray.150'}
py={'1.5rem'}
px={'2rem'}
flex={'1 0 0'}
display={'flex'}
flexDirection={'column'}
overflow={'auto'}
>
{t('account_team:total_team_members', { amount: teamSize })}
{teamTab === TeamTabEnum.member && <MemberTable Tabs={Tabs} />}
{teamTab === TeamTabEnum.org && <OrgManage Tabs={Tabs} />}
{teamTab === TeamTabEnum.group && <GroupManage Tabs={Tabs} />}
{teamTab === TeamTabEnum.permission && <PermissionManage Tabs={Tabs} />}
</Box>
</Flex>
{/* table */}
<Box py={'1.5rem'} px={'2rem'}>
{teamTab === TeamTabEnum.member && <MemberTable Tabs={Tabs} />}
{teamTab === TeamTabEnum.org && <OrgManage Tabs={Tabs} />}
{teamTab === TeamTabEnum.group && <GroupManage Tabs={Tabs} />}
{teamTab === TeamTabEnum.permission && <PermissionManage Tabs={Tabs} />}
</Box>
</AccountContainer>
);
};

View File

@@ -23,15 +23,16 @@ import DateRangePicker, {
import { addDays } from 'date-fns';
import dynamic from 'next/dynamic';
import { useTranslation } from 'next-i18next';
import { useQuery } from '@tanstack/react-query';
import { useUserStore } from '@/web/support/user/useUserStore';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MySelect from '@fastgpt/web/components/common/MySelect';
import { formatNumber } from '@fastgpt/global/common/math/tools';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import AccountContainer, { TabEnum } from '../components/AccountContainer';
import AccountContainer from '../components/AccountContainer';
import { serviceSideProps } from '@fastgpt/web/common/system/nextjs';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import { getTeamMembers } from '@/web/support/user/team/api';
const UsageDetail = dynamic(() => import('./UsageDetail'));
@@ -44,7 +45,7 @@ const UsageTable = () => {
});
const [usageSource, setUsageSource] = useState<UsageSourceEnum | ''>('');
const { isPc } = useSystem();
const { userInfo, loadAndGetTeamMembers } = useUserStore();
const { userInfo } = useUserStore();
const [usageDetail, setUsageDetail] = useState<UsageItemType>();
const sourceList = useMemo(
@@ -63,10 +64,7 @@ const UsageTable = () => {
);
const [selectTmbId, setSelectTmbId] = useState(userInfo?.team?.tmbId);
const { data: members = [] } = useQuery(['getMembers', userInfo?.team?.teamId], () => {
if (!userInfo?.team?.teamId) return [];
return loadAndGetTeamMembers();
});
const { data: members, ScrollData } = useScrollPagination(getTeamMembers, {});
const tmbList = useMemo(
() =>
members.map((item) => ({
@@ -86,14 +84,13 @@ const UsageTable = () => {
isLoading,
Pagination,
getData
} = usePagination<UsageItemType>({
api: getUserUsages,
} = usePagination(getUserUsages, {
pageSize: isPc ? 20 : 10,
params: {
dateStart: dateRange.from || new Date(),
dateEnd: addDays(dateRange.to || new Date(), 1),
source: usageSource,
teamMemberId: selectTmbId
source: usageSource as UsageSourceEnum,
teamMemberId: selectTmbId ?? ''
},
defaultRequest: false
});
@@ -120,6 +117,7 @@ const UsageTable = () => {
<MySelect
size={'sm'}
minW={'100px'}
ScrollData={ScrollData}
list={tmbList}
value={selectTmbId}
onchange={setSelectTmbId}

View File

@@ -5,6 +5,8 @@ import { jiebaSplit } from '@fastgpt/service/common/string/jieba';
import { MongoDatasetDataText } from '@fastgpt/service/core/dataset/data/dataTextSchema';
import { MongoDatasetData } from '@fastgpt/service/core/dataset/data/schema';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { MongoUser } from '@fastgpt/service/support/user/schema';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
import { NextApiRequest, NextApiResponse } from 'next';
/*
@@ -14,6 +16,7 @@ import { NextApiRequest, NextApiResponse } from 'next';
2. 执行升级脚本,不要删除 MongoDatasetData 里的数据。
3. 切换正式版镜像,让 MongoDatasetDataText 生效。
4. 删除 MongoDatasetData 里的索引和多余字段。4819 再删
5. 移动 User 表中的 avatar 字段到 TeamMember 表中。
*/
let success = 0;
async function handler(req: NextApiRequest, res: NextApiResponse) {
@@ -109,15 +112,26 @@ const initData = async (batchSize: number) => {
}
};
const batchUpdateFields = async (batchSize = 2000) => {
// Update in batches
await MongoDatasetData.updateMany(
{ initFullText: { $exists: true } },
{
$unset: {
initFullText: 1,
fullTextToken: 1
}
}
);
};
// const batchUpdateFields = async (batchSize = 2000) => {
// // Find documents that still have these fields
// const documents = await MongoDatasetData.find({ initFullText: { $exists: true } }, '_id')
// .limit(batchSize)
// .lean();
// if (documents.length === 0) return;
// // Update in batches
// await MongoDatasetData.updateMany(
// { _id: { $in: documents.map((doc) => doc._id) } },
// {
// $unset: {
// initFullText: 1
// // fullTextToken: 1
// }
// }
// );
// success += documents.length;
// console.log('Delete success:', success);
// await batchUpdateFields(batchSize);
// };

View File

@@ -0,0 +1,55 @@
import { NextAPI } from '@/service/middleware/entry';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { MongoUser } from '@fastgpt/service/support/user/schema';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
import { NextApiRequest, NextApiResponse } from 'next';
/*
简单版迁移:直接升级到最新镜像,会去除 MongoDatasetData 里的索引。直接执行这个脚本。
无缝迁移:
1. 移动 User 表中的 avatar 字段到 TeamMember 表中。
*/
async function handler(req: NextApiRequest, res: NextApiResponse) {
await authCert({ req, authRoot: true });
await moveUserAvatar();
return { success: true };
}
export default NextAPI(handler);
const moveUserAvatar = async () => {
try {
const users = await MongoUser.find({}, '_id avatar');
console.log('Total users:', users.length);
let success = 0;
for await (const user of users) {
// @ts-ignore
if (!user.avatar) continue;
try {
await mongoSessionRun(async (session) => {
await MongoTeamMember.updateOne(
{
userId: user._id
},
{
$set: {
avatar: (user as any).avatar // 删除 avatar 字段, 因为 Type 改了,所以这里不能直接写 user.avatar
}
},
{ session }
);
// @ts-ignore
user.avatar = undefined;
await user.save({ session });
});
success++;
console.log('Move avatar success:', success);
} catch (error) {
console.error(error);
}
}
} catch (error) {
console.error(error);
}
};

View File

@@ -8,7 +8,8 @@ async function handler(req: ApiRequestProps<{}, { bufferId?: string }>, res: Nex
// If bufferId is the same as the current bufferId, return directly
if (bufferId && global.systemInitBufferId && global.systemInitBufferId === bufferId) {
return {
bufferId: global.systemInitBufferId
bufferId: global.systemInitBufferId,
systemVersion: global.systemVersion || '0.0.0'
};
}

View File

@@ -1,6 +1,5 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
import type { PagingData } from '@/types';
import { AppLogsListItemType } from '@/types/app';
import { Types } from '@fastgpt/service/common/mongo';
import { addDays } from 'date-fns';
@@ -10,19 +9,22 @@ import { ChatItemCollectionName } from '@fastgpt/service/core/chat/chatItemSchem
import { NextAPI } from '@/service/middleware/entry';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { readFromSecondary } from '@fastgpt/service/common/mongo/utils';
import { parsePaginationRequest } from '@fastgpt/service/common/api/pagination';
import { PaginationResponse } from '@fastgpt/web/common/fetch/type';
import { addSourceMember } from '@fastgpt/service/support/user/utils';
async function handler(
req: NextApiRequest,
_res: NextApiResponse
): Promise<PagingData<AppLogsListItemType>> {
): Promise<PaginationResponse<AppLogsListItemType>> {
const {
pageNum = 1,
pageSize = 20,
appId,
dateStart = addDays(new Date(), -7),
dateEnd = new Date()
} = req.body as GetAppChatLogsParams;
const { pageSize = 20, offset } = parsePaginationRequest(req);
if (!appId) {
throw new Error('缺少参数');
}
@@ -39,7 +41,7 @@ async function handler(
}
};
const [data, total] = await Promise.all([
const [list, total] = await Promise.all([
MongoChat.aggregate(
[
{ $match: where },
@@ -51,7 +53,7 @@ async function handler(
updateTime: -1
}
},
{ $skip: (pageNum - 1) * pageSize },
{ $skip: offset },
{ $limit: pageSize },
{
$lookup: {
@@ -144,10 +146,14 @@ async function handler(
MongoChat.countDocuments(where, { ...readFromSecondary })
]);
const listWithSourceMember = await addSourceMember({
list: list
});
const listWithoutTmbId = list.filter((item) => !item.tmbId);
return {
pageNum,
pageSize,
data,
list: listWithSourceMember.concat(listWithoutTmbId),
total
};
}

View File

@@ -18,6 +18,7 @@ import { replaceRegChars } from '@fastgpt/global/common/string/tools';
import { concatPer } from '@fastgpt/service/support/permission/controller';
import { getGroupsByTmbId } from '@fastgpt/service/support/permission/memberGroup/controllers';
import { getOrgIdSetWithParentByTmbId } from '@fastgpt/service/support/permission/org/controllers';
import { addSourceMember } from '@fastgpt/service/support/user/utils';
export type ListAppBody = {
parentId?: ParentIdType;
@@ -201,19 +202,9 @@ async function handler(req: ApiRequestProps<ListAppBody>): Promise<AppListItemTy
})
.filter((app) => app.permission.hasReadPer);
return formatApps.map((app) => ({
_id: app._id,
tmbId: app.tmbId,
avatar: app.avatar,
type: app.type,
name: app.name,
intro: app.intro,
updateTime: app.updateTime,
permission: app.permission,
pluginData: app.pluginData,
inheritPermission: app.inheritPermission ?? true,
private: app.privateApp
}));
return addSourceMember({
list: formatApps
});
}
export default NextAPI(handler);

View File

@@ -6,6 +6,8 @@ import { ApiRequestProps } from '@fastgpt/service/type/next';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { VersionListItemType } from '@fastgpt/global/core/app/version';
import { parsePaginationRequest } from '@fastgpt/service/common/api/pagination';
import { addSourceMember } from '@fastgpt/service/support/user/utils';
export type versionListBody = PaginationProps<{
appId: string;
@@ -15,41 +17,40 @@ export type versionListResponse = PaginationResponse<VersionListItemType>;
async function handler(
req: ApiRequestProps<versionListBody>,
res: NextApiResponse<any>
_res: NextApiResponse<any>
): Promise<versionListResponse> {
const { offset, pageSize, appId } = req.body;
const { appId } = req.body;
const { offset, pageSize } = parsePaginationRequest(req);
await authApp({ appId, req, per: WritePermissionVal, authToken: true });
const [result, total] = await Promise.all([
MongoAppVersion.find(
{
(async () => {
const versions = await MongoAppVersion.find({
appId
},
'_id appId versionName time isPublish tmbId'
)
.sort({
time: -1
})
.skip(offset)
.limit(pageSize),
.sort({
time: -1
})
.skip(offset)
.limit(pageSize)
.lean();
return addSourceMember({
list: versions
}).then((list) =>
list.map((item) => ({
...item,
isPublish: !!item.isPublish
}))
);
})(),
MongoAppVersion.countDocuments({ appId })
]);
const versionList = result.map((item) => {
return {
_id: item._id,
appId: item.appId,
versionName: item.versionName,
time: item.time,
isPublish: item.isPublish,
tmbId: item.tmbId
};
});
return {
total,
list: versionList
list: result
};
}

View File

@@ -12,7 +12,6 @@ describe('发布应用版本测试', () => {
nodes: [],
edges: [],
chatConfig: {},
type: AppTypeEnum.simple,
isPublish: false,
versionName: '1'
};

View File

@@ -164,6 +164,10 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
runningAppInfo: {
id: appId,
teamId: app.teamId,
tmbId: app.tmbId
},
runningUserInfo: {
teamId,
tmbId
},

View File

@@ -7,6 +7,7 @@ import { NextAPI } from '@/service/middleware/entry';
import { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type';
import { GetHistoriesProps } from '@/global/core/chat/api';
import { parsePaginationRequest } from '@fastgpt/service/common/api/pagination';
import { addMonths } from 'date-fns';
export type getHistoriesQuery = {};
@@ -17,9 +18,10 @@ export type getHistoriesResponse = {};
async function handler(
req: ApiRequestProps<getHistoriesBody, getHistoriesQuery>,
res: ApiResponseType<any>
_res: ApiResponseType<any>
): Promise<PaginationResponse<getHistoriesResponse>> {
const { appId, shareId, outLinkUid, teamId, teamToken, offset, pageSize, source } = req.body;
const { appId, shareId, outLinkUid, teamId, teamToken, source } = req.body;
const { offset, pageSize } = parsePaginationRequest(req);
const match = await (async () => {
if (shareId && outLinkUid) {

View File

@@ -13,6 +13,7 @@ import { filterPublicNodeResponseData } from '@fastgpt/global/core/chat/utils';
import { GetChatTypeEnum } from '@/global/core/chat/constants';
import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type';
import { ChatItemType } from '@fastgpt/global/core/chat/type';
import { parsePaginationRequest } from '@fastgpt/service/common/api/pagination';
export type getPaginationRecordsQuery = {};
@@ -22,16 +23,11 @@ export type getPaginationRecordsResponse = PaginationResponse<ChatItemType>;
async function handler(
req: ApiRequestProps<getPaginationRecordsBody, getPaginationRecordsQuery>,
res: ApiResponseType<any>
_res: ApiResponseType<any>
): Promise<getPaginationRecordsResponse> {
const {
appId,
chatId,
offset,
pageSize = 10,
loadCustomFeedbacks,
type = GetChatTypeEnum.normal
} = req.body;
const { appId, chatId, loadCustomFeedbacks, type = GetChatTypeEnum.normal } = req.body;
const { offset, pageSize } = parsePaginationRequest(req);
if (!appId || !chatId) {
return {

View File

@@ -6,6 +6,7 @@ import { ApiRequestProps } from '@fastgpt/service/type/next';
import { ChatInputGuideSchemaType } from '@fastgpt/global/core/chat/inputGuide/type';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { parsePaginationRequest } from '@fastgpt/service/common/api/pagination';
export type ChatInputGuideProps = PaginationProps<{
appId: string;
@@ -17,7 +18,8 @@ async function handler(
req: ApiRequestProps<ChatInputGuideProps>,
res: NextApiResponse<any>
): Promise<ChatInputGuideResponse> {
const { appId, pageSize, offset, searchKey } = req.body;
const { appId, searchKey } = req.body;
const { offset, pageSize } = parsePaginationRequest(req);
await authApp({ req, appId, authToken: true, per: ReadPermissionVal });

View File

@@ -2,7 +2,6 @@ import type { NextApiRequest } from 'next';
import { DatasetTrainingCollectionName } from '@fastgpt/service/core/dataset/training/schema';
import { Types } from '@fastgpt/service/common/mongo';
import type { DatasetCollectionsListItemType } from '@/global/core/dataset/type.d';
import type { GetDatasetCollectionsProps } from '@/global/core/api/datasetReq';
import { MongoDatasetCollection } from '@fastgpt/service/core/dataset/collection/schema';
import { DatasetCollectionTypeEnum } from '@fastgpt/global/core/dataset/constants';
import { authDataset } from '@fastgpt/service/support/permission/dataset/auth';
@@ -10,11 +9,10 @@ import { DatasetDataCollectionName } from '@fastgpt/service/core/dataset/data/sc
import { startTrainingQueue } from '@/service/core/dataset/training/utils';
import { NextAPI } from '@/service/middleware/entry';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { PagingData } from '@/types';
import { readFromSecondary } from '@fastgpt/service/common/mongo/utils';
import { collectionTagsToTagLabel } from '@fastgpt/service/core/dataset/collection/utils';
async function handler(req: NextApiRequest): Promise<PagingData<DatasetCollectionsListItemType>> {
async function handler(req: NextApiRequest) {
let {
pageNum = 1,
pageSize = 10,
@@ -24,7 +22,7 @@ async function handler(req: NextApiRequest): Promise<PagingData<DatasetCollectio
selectFolder = false,
filterTags = [],
simple = false
} = req.body as GetDatasetCollectionsProps;
} = req.body as any;
searchText = searchText?.replace(/'/g, '');
pageSize = Math.min(pageSize, 30);

View File

@@ -0,0 +1,192 @@
import type { NextApiRequest } from 'next';
import { DatasetTrainingCollectionName } from '@fastgpt/service/core/dataset/training/schema';
import { Types } from '@fastgpt/service/common/mongo';
import type { DatasetCollectionsListItemType } from '@/global/core/dataset/type.d';
import type { GetDatasetCollectionsProps } from '@/global/core/api/datasetReq';
import { MongoDatasetCollection } from '@fastgpt/service/core/dataset/collection/schema';
import { DatasetCollectionTypeEnum } from '@fastgpt/global/core/dataset/constants';
import { authDataset } from '@fastgpt/service/support/permission/dataset/auth';
import { DatasetDataCollectionName } from '@fastgpt/service/core/dataset/data/schema';
import { startTrainingQueue } from '@/service/core/dataset/training/utils';
import { NextAPI } from '@/service/middleware/entry';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { readFromSecondary } from '@fastgpt/service/common/mongo/utils';
import { collectionTagsToTagLabel } from '@fastgpt/service/core/dataset/collection/utils';
import { PaginationResponse } from '@fastgpt/web/common/fetch/type';
import { parsePaginationRequest } from '@fastgpt/service/common/api/pagination';
async function handler(
req: NextApiRequest
): Promise<PaginationResponse<DatasetCollectionsListItemType>> {
let {
datasetId,
parentId = null,
searchText = '',
selectFolder = false,
filterTags = [],
simple = false
} = req.body as GetDatasetCollectionsProps;
let { pageSize, offset } = parsePaginationRequest(req);
pageSize = Math.min(pageSize, 30);
searchText = searchText?.replace(/'/g, '');
// auth dataset and get my role
const { teamId, permission } = await authDataset({
req,
authToken: true,
authApiKey: true,
datasetId,
per: ReadPermissionVal
});
const match = {
teamId: new Types.ObjectId(teamId),
datasetId: new Types.ObjectId(datasetId),
parentId: parentId ? new Types.ObjectId(parentId) : null,
...(selectFolder ? { type: DatasetCollectionTypeEnum.folder } : {}),
...(searchText
? {
name: new RegExp(searchText, 'i')
}
: {}),
...(filterTags.length ? { tags: { $in: filterTags } } : {})
};
const selectField = {
_id: 1,
parentId: 1,
tmbId: 1,
name: 1,
type: 1,
forbid: 1,
createTime: 1,
updateTime: 1,
trainingType: 1,
fileId: 1,
rawLink: 1,
tags: 1,
externalFileId: 1
};
// not count data amount
if (simple) {
const collections = await MongoDatasetCollection.find(match, undefined, {
...readFromSecondary
})
.select(selectField)
.sort({
updateTime: -1
})
.lean();
return {
list: await Promise.all(
collections.map(async (item) => ({
...item,
tags: await collectionTagsToTagLabel({
datasetId,
tags: item.tags
}),
dataAmount: 0,
trainingAmount: 0,
permission
}))
),
total: await MongoDatasetCollection.countDocuments(match)
};
}
const [collections, total]: [DatasetCollectionsListItemType[], number] = await Promise.all([
MongoDatasetCollection.aggregate([
{
$match: match
},
{
$sort: { updateTime: -1 }
},
{
$skip: offset
},
{
$limit: pageSize
},
// count training data
{
$lookup: {
from: DatasetTrainingCollectionName,
let: { id: '$_id', team_id: match.teamId, dataset_id: match.datasetId },
pipeline: [
{
$match: {
$expr: {
$and: [{ $eq: ['$teamId', '$$team_id'] }, { $eq: ['$collectionId', '$$id'] }]
}
}
},
{ $count: 'count' }
],
as: 'trainingCount'
}
},
// count collection total data
{
$lookup: {
from: DatasetDataCollectionName,
let: { id: '$_id', team_id: match.teamId, dataset_id: match.datasetId },
pipeline: [
{
$match: {
$expr: {
$and: [
{ $eq: ['$teamId', '$$team_id'] },
{ $eq: ['$datasetId', '$$dataset_id'] },
{ $eq: ['$collectionId', '$$id'] }
]
}
}
},
{ $count: 'count' }
],
as: 'dataCount'
}
},
{
$project: {
...selectField,
dataAmount: {
$ifNull: [{ $arrayElemAt: ['$dataCount.count', 0] }, 0]
},
trainingAmount: {
$ifNull: [{ $arrayElemAt: ['$trainingCount.count', 0] }, 0]
}
}
}
]),
MongoDatasetCollection.countDocuments(match, {
...readFromSecondary
})
]);
const list = await Promise.all(
collections.map(async (item) => ({
...item,
tags: await collectionTagsToTagLabel({
datasetId,
tags: item.tags
}),
permission
}))
);
if (list.find((item) => item.trainingAmount > 0)) {
startTrainingQueue();
}
// count collections
return {
list,
total
};
}
export default NextAPI(handler);

View File

@@ -10,6 +10,7 @@ import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
import { ApiRequestProps } from '@fastgpt/service/type/next';
import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type';
import type { DatasetCollectionsListItemType } from '@/global/core/dataset/type.d';
import { parsePaginationRequest } from '@fastgpt/service/common/api/pagination';
export type GetScrollCollectionsProps = PaginationProps<{
datasetId: string;
@@ -25,8 +26,6 @@ async function handler(
): Promise<PaginationResponse<DatasetCollectionsListItemType>> {
let {
datasetId,
pageSize = 10,
offset,
parentId = null,
searchText = '',
selectFolder = false,
@@ -36,6 +35,7 @@ async function handler(
if (!datasetId) {
return Promise.reject(CommonErrEnum.missingParams);
}
let { offset, pageSize } = parsePaginationRequest(req);
searchText = searchText?.replace(/'/g, '');
pageSize = Math.min(pageSize, 30);

View File

@@ -3,19 +3,21 @@ import { MongoDatasetData } from '@fastgpt/service/core/dataset/data/schema';
import { replaceRegChars } from '@fastgpt/global/common/string/tools';
import { NextAPI } from '@/service/middleware/entry';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { PagingData, RequestPaging } from '@/types';
import { ApiRequestProps } from '@fastgpt/service/type/next';
import { DatasetDataListItemType } from '@/global/core/dataset/type';
import { parsePaginationRequest } from '@fastgpt/service/common/api/pagination';
import { PaginationResponse } from '@fastgpt/web/common/fetch/type';
export type GetDatasetDataListProps = RequestPaging & {
export type GetDatasetDataListProps = {
searchText?: string;
collectionId: string;
};
async function handler(
req: ApiRequestProps<GetDatasetDataListProps>
): Promise<PagingData<DatasetDataListItemType>> {
let { pageNum = 1, pageSize = 10, searchText = '', collectionId } = req.body;
): Promise<PaginationResponse<DatasetDataListItemType>> {
let { searchText = '', collectionId } = req.body;
let { offset, pageSize } = parsePaginationRequest(req);
pageSize = Math.min(pageSize, 30);
@@ -40,19 +42,17 @@ async function handler(
: {})
};
const [data, total] = await Promise.all([
const [list, total] = await Promise.all([
MongoDatasetData.find(match, '_id datasetId collectionId q a chunkIndex')
.sort({ chunkIndex: 1, updateTime: -1 })
.skip((pageNum - 1) * pageSize)
.skip(offset)
.limit(pageSize)
.lean(),
MongoDatasetData.countDocuments(match)
]);
return {
pageNum,
pageSize,
data,
list,
total
};
}

View File

@@ -6,6 +6,7 @@ import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { ApiRequestProps } from '@fastgpt/service/type/next';
import { DatasetDataListItemType } from '@/global/core/dataset/type';
import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type';
import { parsePaginationRequest } from '@fastgpt/service/common/api/pagination';
export type GetDatasetDataListProps = PaginationProps & {
searchText?: string;
@@ -16,7 +17,8 @@ export type GetDatasetDataListRes = PaginationResponse<DatasetDataListItemType>;
async function handler(
req: ApiRequestProps<GetDatasetDataListProps>
): Promise<GetDatasetDataListRes> {
let { offset, pageSize = 10, searchText = '', collectionId } = req.body;
let { searchText = '', collectionId } = req.body;
let { offset, pageSize } = parsePaginationRequest(req);
pageSize = Math.min(pageSize, 30);

View File

@@ -1,8 +1,6 @@
import type { DatasetListItemType } from '@fastgpt/global/core/dataset/type.d';
import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants';
import { MongoDataset } from '@fastgpt/service/core/dataset/schema';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { getVectorModel } from '@fastgpt/service/core/ai/model';
import { NextAPI } from '@/service/middleware/entry';
import { DatasetPermission } from '@fastgpt/global/support/permission/dataset/controller';
import {
@@ -19,6 +17,8 @@ import { replaceRegChars } from '@fastgpt/global/common/string/tools';
import { getGroupsByTmbId } from '@fastgpt/service/support/permission/memberGroup/controllers';
import { concatPer } from '@fastgpt/service/support/permission/controller';
import { getOrgIdSetWithParentByTmbId } from '@fastgpt/service/support/permission/org/controllers';
import { addSourceMember } from '@fastgpt/service/support/user/utils';
import { getVectorModel } from '@fastgpt/service/core/ai/model';
export type GetDatasetListBody = {
parentId: ParentIdType;
@@ -167,28 +167,24 @@ async function handler(req: ApiRequestProps<GetDatasetListBody>) {
})();
return {
...dataset,
_id: dataset._id,
avatar: dataset.avatar,
name: dataset.name,
intro: dataset.intro,
type: dataset.type,
vectorModel: getVectorModel(dataset.vectorModel),
inheritPermission: dataset.inheritPermission,
tmbId: dataset.tmbId,
updateTime: dataset.updateTime,
permission: Per,
privateDataset
};
})
.filter((app) => app.permission.hasReadPer);
const data = formatDatasets.map<DatasetListItemType>((item) => ({
_id: item._id,
avatar: item.avatar,
name: item.name,
intro: item.intro,
type: item.type,
permission: item.permission,
vectorModel: getVectorModel(item.vectorModel),
inheritPermission: item.inheritPermission,
tmbId: item.tmbId,
updateTime: item.updateTime,
private: item.privateDataset
}));
return data;
return addSourceMember({
list: formatDatasets
});
}
export default NextAPI(handler);

View File

@@ -45,7 +45,11 @@ async function handler(
requestOrigin: req.headers.origin,
mode: 'debug',
runningAppInfo: {
id: appId,
id: app._id,
teamId: app.teamId,
tmbId: app.tmbId
},
runningUserInfo: {
teamId,
tmbId
},

View File

@@ -1,17 +1,18 @@
import { MongoUser } from '@fastgpt/service/support/user/schema';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { UserUpdateParams } from '@/types/user';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
/* update user info */
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { getUserDetail } from '@fastgpt/service/support/user/controller';
import { refreshSourceAvatar } from '@fastgpt/service/common/file/image/controller';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
export type UserAccountUpdateQuery = {};
export type UserAccountUpdateBody = UserUpdateParams;
export type UserAccountUpdateResponse = {};
async function handler(
req: ApiRequestProps<UserAccountUpdateBody, UserAccountUpdateQuery>,
_res: ApiResponseType<any>
@@ -19,21 +20,33 @@ async function handler(
const { avatar, timezone } = req.body;
const { tmbId } = await authCert({ req, authToken: true });
const user = await getUserDetail({ tmbId });
// const user = await getUserDetail({ tmbId });
// 更新对应的记录
await mongoSessionRun(async (session) => {
await MongoUser.updateOne(
{
_id: user._id
},
{
...(avatar && { avatar }),
...(timezone && { timezone })
}
).session(session);
await refreshSourceAvatar(avatar, user.avatar, session);
const tmb = await MongoTeamMember.findById(tmbId).session(session);
if (timezone) {
await MongoUser.updateOne(
{
_id: tmb?.userId
},
{
timezone
}
).session(session);
}
// if avatar, update team member avatar
if (avatar) {
await MongoTeamMember.updateOne(
{
_id: tmbId
},
{
avatar
}
).session(session);
await refreshSourceAvatar(avatar, tmb?.avatar, session);
}
});
return {};

View File

@@ -280,6 +280,10 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
teamId: String(app.teamId),
tmbId: String(app.tmbId)
},
runningUserInfo: {
teamId,
tmbId
},
uid: String(outLinkUserId || tmbId),
chatId,

View File

@@ -164,8 +164,6 @@ const DetailLogsModal = ({ appId, chatId, onClose }: Props) => {
showMarkIcon
showVoiceIcon={false}
chatType="log"
showRawSource
showNodeStatus
/>
)}
</Box>
@@ -187,7 +185,12 @@ const Render = (props: Props) => {
}, [appId, chatId]);
return (
<ChatItemContextProvider>
<ChatItemContextProvider
showRouteToAppDetail={true}
showRouteToDatasetDetail={true}
isShowReadRawSource={true}
showNodeStatus
>
<ChatRecordContextProvider params={params}>
<DetailLogsModal {...props} />
</ChatRecordContextProvider>

View File

@@ -13,7 +13,7 @@ import {
ModalBody,
HStack
} from '@chakra-ui/react';
import Avatar from '@fastgpt/web/components/common/Avatar';
import UserBox from '@fastgpt/web/components/common/UserBox';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
import { getAppChatLogs } from '@/web/core/app/api';
@@ -30,8 +30,6 @@ import { cardStyles } from '../constants';
import dynamic from 'next/dynamic';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useMount } from 'ahooks';
const DetailLogsModal = dynamic(() => import('./DetailLogsModal'));
@@ -40,17 +38,11 @@ const Logs = () => {
const { isPc } = useSystem();
const appId = useContextSelector(AppContext, (v) => v.appId);
const { teamMembers, loadAndGetTeamMembers } = useUserStore();
useMount(() => {
loadAndGetTeamMembers();
});
const [dateRange, setDateRange] = useState<DateRangeType>({
from: addDays(new Date(), -7),
to: new Date()
});
const {
isOpen: isOpenMarkDesc,
onOpen: onOpenMarkDesc,
@@ -63,8 +55,7 @@ const Logs = () => {
Pagination,
getData,
pageNum
} = usePagination({
api: getAppChatLogs,
} = usePagination(getAppChatLogs, {
pageSize: 20,
params: {
appId,
@@ -139,15 +130,7 @@ const Logs = () => {
{!!item.outLinkUid ? (
item.outLinkUid
) : (
<HStack>
<Avatar
src={teamMembers?.find((v) => v.tmbId === item.tmbId)?.avatar}
w="1.25rem"
/>
<Box fontSize={'sm'} ml={1}>
{teamMembers?.find((v) => v.tmbId === item.tmbId)?.memberName}
</Box>
</HStack>
<UserBox sourceMember={item.sourceMember} />
)}
</Box>
</Td>

View File

@@ -4,7 +4,7 @@ import {
getWorkflowVersionList,
updateAppVersion
} from '@/web/core/app/api/version';
import { useVirtualScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import CustomRightDrawer from '@fastgpt/web/components/common/MyDrawer/CustomRightDrawer';
import { useTranslation } from 'next-i18next';
import { Box, BoxProps, Button, Flex, Input } from '@chakra-ui/react';
@@ -18,9 +18,7 @@ import Tag from '@fastgpt/web/components/common/Tag';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyPopover from '@fastgpt/web/components/common/MyPopover';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useToast } from '@fastgpt/web/hooks/useToast';
import type { AppVersionSchemaType, VersionListItemType } from '@fastgpt/global/core/app/version';
import type { SimpleAppSnapshotType } from './SimpleApp/useSnapshots';
@@ -183,23 +181,18 @@ const TeamCloud = ({
}) => {
const { t } = useTranslation();
const { appDetail } = useContextSelector(AppContext, (v) => v);
const { loadAndGetTeamMembers } = useUserStore();
const { feConfigs } = useSystemStore();
const { scrollDataList, ScrollList, isLoading, fetchData, setData } = useVirtualScrollPagination(
getWorkflowVersionList,
{
itemHeight: 40,
overscan: 20,
pageSize: 30,
defaultParams: {
appId: appDetail._id
}
}
);
const { data: members = [] } = useRequest2(loadAndGetTeamMembers, {
manual: !feConfigs.isPlus
const {
ScrollData,
data: scrollDataList,
setData,
isLoading
} = useScrollPagination(getWorkflowVersionList, {
pageSize: 30,
params: {
appId: appDetail._id
},
refreshDeps: [appDetail._id]
});
const [editIndex, setEditIndex] = useState<number | undefined>(undefined);
const [hoveredIndex, setHoveredIndex] = useState<number | undefined>(undefined);
@@ -237,15 +230,13 @@ const TeamCloud = ({
);
return (
<ScrollList isLoading={isLoading || isLoadingVersion} flex={'1 0 0'} px={5}>
{scrollDataList.map((data, index) => {
const item = data.data;
const firstPublishedIndex = scrollDataList.findIndex((data) => data.data.isPublish);
const tmb = members.find((member) => member.tmbId === item.tmbId);
<ScrollData isLoading={isLoading || isLoadingVersion} flex={'1 0 0'} px={5}>
{scrollDataList.map((item, index) => {
const firstPublishedIndex = scrollDataList.findIndex((data) => data.isPublish);
return (
<Flex
key={data.index}
key={item._id}
alignItems={'center'}
py={editIndex !== index ? 2 : 1}
px={3}
@@ -266,20 +257,33 @@ const TeamCloud = ({
h={'72px'}
Trigger={
<Box>
<Avatar src={tmb?.avatar} borderRadius={'50%'} w={'24px'} h={'24px'} />
<Avatar
src={item.sourceMember.avatar}
borderRadius={'50%'}
w={'24px'}
h={'24px'}
/>
</Box>
}
>
{({ onClose }) => (
<Flex alignItems={'center'} h={'full'} pl={5} gap={3}>
{() => (
<Flex alignItems={'center'} h={'full'} pl={5} gap={2}>
<Box>
<Avatar src={tmb?.avatar} borderRadius={'50%'} w={'36px'} h={'36px'} />
<Avatar
src={item.sourceMember.avatar}
borderRadius={'50%'}
w={'36px'}
h={'36px'}
/>
</Box>
<Box>
<Box fontSize={'14px'} color={'myGray.900'}>
{tmb?.memberName}
</Box>
<Box fontSize={'12px'} color={'myGray.500'}>
<Flex gap={1} fontSize={'sm'} color={'myGray.900'}>
<Box>{item.sourceMember.name}</Box>
{item.sourceMember.status === 'leave' && (
<Tag color="gray">{t('common:user_leaved')}</Tag>
)}
</Flex>
<Box fontSize={'xs'} mt={2} color={'myGray.500'}>
{formatTime2YMDHMS(item.time)}
</Box>
</Box>
@@ -349,6 +353,6 @@ const TeamCloud = ({
</Flex>
);
})}
</ScrollList>
</ScrollData>
);
};

View File

@@ -91,7 +91,12 @@ const Render = ({ appForm }: Props) => {
);
return (
<ChatItemContextProvider>
<ChatItemContextProvider
showRouteToAppDetail={true}
showRouteToDatasetDetail={true}
isShowReadRawSource={true}
showNodeStatus
>
<ChatRecordContextProvider params={chatRecordProviderParams}>
<ChatTest appForm={appForm} />
</ChatRecordContextProvider>

View File

@@ -158,7 +158,12 @@ const Render = (Props: Props) => {
);
return (
<ChatItemContextProvider>
<ChatItemContextProvider
showRouteToAppDetail={true}
showRouteToDatasetDetail={true}
isShowReadRawSource={true}
showNodeStatus
>
<ChatRecordContextProvider params={chatRecordProviderParams}>
<ChatTest {...Props} />
</ChatRecordContextProvider>

View File

@@ -50,7 +50,6 @@ import { useWorkflowUtils } from './hooks/useUtils';
import { moduleTemplatesFlat } from '@fastgpt/global/core/workflow/template/constants';
import { cloneDeep } from 'lodash';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { useUserStore } from '@/web/support/user/useUserStore';
import { LoopStartNode } from '@fastgpt/global/core/workflow/template/system/loop/loopStart';
import { LoopEndNode } from '@fastgpt/global/core/workflow/template/system/loop/loopEnd';
import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
@@ -89,8 +88,6 @@ enum TemplateTypeEnum {
const sliderWidth = 460;
const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
const { loadAndGetTeamMembers } = useUserStore();
const [parentId, setParentId] = useState<ParentIdType>('');
const [searchKey, setSearchKey] = useState('');
const { feConfigs } = useSystemStore();
@@ -99,10 +96,6 @@ const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const appId = useContextSelector(WorkflowContext, (v) => v.appId);
const { data: members = [] } = useRequest2(loadAndGetTeamMembers, {
manual: !feConfigs.isPlus
});
const [templateType, setTemplateType] = useState(TemplateTypeEnum.basic);
const { data: basicNodes } = useRequest2(
@@ -162,19 +155,10 @@ const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
searchVal?: string;
}) => {
if (type === TemplateTypeEnum.teamPlugin) {
const teamApps = await getTeamPlugTemplates({
return getTeamPlugTemplates({
parentId,
searchKey: searchVal
}).then((res) => res.filter((app) => app.id !== appId));
return teamApps.map<NodeTemplateListItemType>((app) => {
const member = members.find((member) => member.tmbId === app.tmbId);
return {
...app,
author: member?.memberName,
authorAvatar: member?.avatar
};
});
}
if (type === TemplateTypeEnum.systemPlugin) {
return getSystemPlugTemplates({
@@ -188,7 +172,7 @@ const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
setParentId(parentId);
setTemplateType(type);
},
refreshDeps: [members, searchKey, templateType]
refreshDeps: [searchKey, templateType]
}
);
@@ -420,7 +404,6 @@ const RenderList = React.memo(function RenderList({
templates,
type,
onClose,
parentId,
setParentId
}: RenderListProps) {
const { t } = useTranslation();

View File

@@ -14,22 +14,18 @@ import RenderToolInput from './render/RenderToolInput';
import RenderOutput from './render/RenderOutput';
import CodeEditor from '@fastgpt/web/components/common/Textarea/CodeEditor';
import { Box, Flex } from '@chakra-ui/react';
import { useI18n } from '@/web/context/I18n';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { getLatestNodeTemplate } from '@/web/core/workflow/utils';
import { CodeNode } from '@fastgpt/global/core/workflow/template/system/sandbox';
import { JS_TEMPLATE } from '@fastgpt/global/core/workflow/template/system/sandbox/constants';
const NodeCode = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { workflowT } = useI18n();
const { nodeId, inputs, outputs } = data;
const { splitToolInputs, onChangeNode, onResetNode } = useContextSelector(
WorkflowContext,
(ctx) => ctx
);
const splitToolInputs = useContextSelector(WorkflowContext, (ctx) => ctx.splitToolInputs);
const onChangeNode = useContextSelector(WorkflowContext, (ctx) => ctx.onChangeNode);
const { ConfirmModal, openConfirm } = useConfirm({
content: workflowT('code.Reset template confirm')
content: t('workflow:code.Reset template confirm')
});
const CustomComponent = useMemo(() => {
@@ -38,19 +34,24 @@ const NodeCode = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
return (
<Box mt={-3}>
<Flex mb={2} alignItems={'flex-end'}>
<Box flex={'1'}>{'Javascript ' + workflowT('Code')}</Box>
<Box flex={'1'}>{'Javascript ' + t('workflow:Code')}</Box>
<Box
cursor={'pointer'}
color={'primary.500'}
fontSize={'xs'}
onClick={openConfirm(() => {
onResetNode({
id: nodeId,
node: getLatestNodeTemplate(data, CodeNode)
onChangeNode({
nodeId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: JS_TEMPLATE
}
});
})}
>
{workflowT('code.Reset template')}
{t('workflow:code.Reset template')}
</Box>
</Flex>
<CodeEditor
@@ -73,37 +74,33 @@ const NodeCode = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
);
}
};
}, [data, nodeId, onChangeNode, onResetNode, openConfirm, workflowT]);
}, [nodeId, onChangeNode, openConfirm, t]);
const Render = useMemo(() => {
const { isTool, commonInputs } = splitToolInputs(inputs, nodeId);
const { isTool, commonInputs } = splitToolInputs(inputs, nodeId);
return (
<NodeCard minW={'400px'} selected={selected} {...data}>
{isTool && (
<>
<Container>
<RenderToolInput nodeId={nodeId} inputs={inputs} />
</Container>
</>
)}
<Container>
<IOTitle text={t('common:common.Input')} mb={-1} />
<RenderInput
nodeId={nodeId}
flowInputList={commonInputs}
CustomComponent={CustomComponent}
/>
</Container>
<Container>
<IOTitle text={t('common:common.Output')} />
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
</Container>
<ConfirmModal />
</NodeCard>
);
}, [ConfirmModal, CustomComponent, data, inputs, nodeId, outputs, selected, splitToolInputs, t]);
return Render;
return (
<NodeCard minW={'400px'} selected={selected} {...data}>
{isTool && (
<>
<Container>
<RenderToolInput nodeId={nodeId} inputs={inputs} />
</Container>
</>
)}
<Container>
<IOTitle text={t('common:common.Input')} mb={-1} />
<RenderInput
nodeId={nodeId}
flowInputList={commonInputs}
CustomComponent={CustomComponent}
/>
</Container>
<Container>
<IOTitle text={t('common:common.Output')} />
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
</Container>
<ConfirmModal />
</NodeCard>
);
};
export default React.memo(NodeCode);

View File

@@ -1,5 +1,5 @@
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
import React, { useCallback, useMemo } from 'react';
import React, { useCallback } from 'react';
import { useTranslation } from 'next-i18next';
import { Box, Flex } from '@chakra-ui/react';
@@ -10,14 +10,14 @@ import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponents/context';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import VariableTip from '@/components/common/Textarea/MyTextarea/VariableTip';
type Props = {
nodeId: string;
input: FlowNodeInputItemType;
RightComponent?: React.JSX.Element;
};
const InputLabel = ({ nodeId, input }: Props) => {
const InputLabel = ({ nodeId, input, RightComponent }: Props) => {
const { t } = useTranslation();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
@@ -68,11 +68,11 @@ const InputLabel = ({ nodeId, input }: Props) => {
</Box>
)}
{/* Variable picker tip */}
{input.renderTypeList[input.selectedTypeIndex ?? 0] === FlowNodeInputTypeEnum.textarea && (
{/* Right Component */}
{RightComponent && (
<>
<Box flex={1} />
<VariableTip transform={'translateY(2px)'} />
<Box flex={'1'} />
{RightComponent}
</>
)}
</Flex>

View File

@@ -8,71 +8,72 @@ import InputLabel from './Label';
import type { RenderInputProps } from './type';
import { useSystemStore } from '@/web/common/system/useSystemStore';
const RenderList: {
types: FlowNodeInputTypeEnum[];
Component: React.ComponentType<RenderInputProps>;
}[] = [
{
types: [FlowNodeInputTypeEnum.reference],
const RenderList: Record<
FlowNodeInputTypeEnum,
| {
Component: React.ComponentType<RenderInputProps>;
LableRightComponent?: React.ComponentType<RenderInputProps>;
}
| undefined
> = {
[FlowNodeInputTypeEnum.reference]: {
Component: dynamic(() => import('./templates/Reference'))
},
{
types: [FlowNodeInputTypeEnum.fileSelect],
[FlowNodeInputTypeEnum.fileSelect]: {
Component: dynamic(() => import('./templates/Reference'))
},
{
types: [FlowNodeInputTypeEnum.select],
[FlowNodeInputTypeEnum.select]: {
Component: dynamic(() => import('./templates/Select'))
},
{
types: [FlowNodeInputTypeEnum.numberInput],
[FlowNodeInputTypeEnum.numberInput]: {
Component: dynamic(() => import('./templates/NumberInput'))
},
{
types: [FlowNodeInputTypeEnum.switch],
[FlowNodeInputTypeEnum.switch]: {
Component: dynamic(() => import('./templates/Switch'))
},
{
types: [FlowNodeInputTypeEnum.selectApp],
[FlowNodeInputTypeEnum.selectApp]: {
Component: dynamic(() => import('./templates/SelectApp'))
},
{
types: [FlowNodeInputTypeEnum.selectLLMModel],
[FlowNodeInputTypeEnum.selectLLMModel]: {
Component: dynamic(() => import('./templates/SelectLLMModel'))
},
{
types: [FlowNodeInputTypeEnum.settingLLMModel],
[FlowNodeInputTypeEnum.settingLLMModel]: {
Component: dynamic(() => import('./templates/SettingLLMModel'))
},
{
types: [FlowNodeInputTypeEnum.selectDataset],
Component: dynamic(() => import('./templates/SelectDataset'))
[FlowNodeInputTypeEnum.selectDataset]: {
Component: dynamic(() =>
import('./templates/SelectDataset').then((mod) => mod.SelectDatasetRender)
),
LableRightComponent: dynamic(() =>
import('./templates/SelectDataset').then((mod) => mod.SwitchAuthTmb)
)
},
{
types: [FlowNodeInputTypeEnum.selectDatasetParamsModal],
[FlowNodeInputTypeEnum.selectDatasetParamsModal]: {
Component: dynamic(() => import('./templates/SelectDatasetParams'))
},
{
types: [FlowNodeInputTypeEnum.addInputParam],
[FlowNodeInputTypeEnum.addInputParam]: {
Component: dynamic(() => import('./templates/DynamicInputs/index'))
},
{
types: [FlowNodeInputTypeEnum.JSONEditor],
[FlowNodeInputTypeEnum.JSONEditor]: {
Component: dynamic(() => import('./templates/JsonEditor'))
},
{
types: [FlowNodeInputTypeEnum.settingDatasetQuotePrompt],
[FlowNodeInputTypeEnum.settingDatasetQuotePrompt]: {
Component: dynamic(() => import('./templates/SettingQuotePrompt'))
},
{
types: [FlowNodeInputTypeEnum.input],
[FlowNodeInputTypeEnum.input]: {
Component: dynamic(() => import('./templates/TextInput'))
},
{
types: [FlowNodeInputTypeEnum.textarea],
Component: dynamic(() => import('./templates/Textarea'))
}
];
[FlowNodeInputTypeEnum.textarea]: {
Component: dynamic(() => import('./templates/Textarea')),
LableRightComponent: dynamic(() =>
import('./templates/Textarea').then((mod) => mod.TextareaRightComponent)
)
},
[FlowNodeInputTypeEnum.customVariable]: undefined,
[FlowNodeInputTypeEnum.hidden]: undefined,
[FlowNodeInputTypeEnum.custom]: undefined
};
const hideLabelTypeList = [FlowNodeInputTypeEnum.addInputParam];
@@ -101,7 +102,7 @@ const RenderInput = ({ flowInputList, nodeId, CustomComponent, mb = 5 }: Props)
return true;
});
}, [feConfigs?.isPlus, flowInputList]);
}, [filterProInputs]);
return (
<>
@@ -110,23 +111,41 @@ const RenderInput = ({ flowInputList, nodeId, CustomComponent, mb = 5 }: Props)
const RenderComponent = (() => {
if (renderType === FlowNodeInputTypeEnum.custom && CustomComponent?.[input.key]) {
return <>{CustomComponent?.[input.key]({ ...input })}</>;
return {
Component: <>{CustomComponent?.[input.key]({ ...input })}</>
};
}
const Component = RenderList.find((item) => item.types.includes(renderType))?.Component;
const RenderItem = RenderList[renderType];
if (!Component) return null;
return <Component inputs={filterProInputs} item={input} nodeId={nodeId} />;
if (!RenderItem) return null;
return {
Component: (
<RenderItem.Component inputs={filterProInputs} item={input} nodeId={nodeId} />
),
LableRightComponent: RenderItem.LableRightComponent ? (
<RenderItem.LableRightComponent
inputs={filterProInputs}
item={input}
nodeId={nodeId}
/>
) : undefined
};
})();
return (
<Box key={input.key} _notLast={{ mb }} position={'relative'}>
{!!input.label && !hideLabelTypeList.includes(renderType) && (
<InputLabel nodeId={nodeId} input={input} />
<InputLabel
nodeId={nodeId}
input={input}
RightComponent={RenderComponent?.LableRightComponent}
/>
)}
{!!RenderComponent && (
<Box mt={2} className={'nodrag'}>
{RenderComponent}
{RenderComponent.Component}
</Box>
)}
</Box>

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useMemo, useState } from 'react';
import type { RenderInputProps } from '../type';
import { Box, Button, Flex, Grid, useDisclosure, useTheme } from '@chakra-ui/react';
import { Box, Button, Flex, Grid, Switch, useDisclosure, useTheme } from '@chakra-ui/react';
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
import { SelectedDatasetType } from '@fastgpt/global/core/workflow/api';
import Avatar from '@fastgpt/web/components/common/Avatar';
@@ -12,12 +12,17 @@ import dynamic from 'next/dynamic';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponents/context';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
const DatasetSelectModal = dynamic(() => import('@/components/core/app/DatasetSelectModal'));
const SelectDatasetRender = ({ inputs = [], item, nodeId }: RenderInputProps) => {
export const SelectDatasetRender = React.memo(function SelectDatasetRender({
inputs = [],
item,
nodeId
}: RenderInputProps) {
const { t } = useTranslation();
const theme = useTheme();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const [data, setData] = useState({
@@ -80,8 +85,9 @@ const SelectDatasetRender = ({ inputs = [], item, nodeId }: RenderInputProps) =>
key={item._id}
alignItems={'center'}
h={10}
border={theme.borders.base}
borderColor={'myGray.200'}
boxShadow={'sm'}
bg={'white'}
border={'base'}
px={2}
borderRadius={'md'}
>
@@ -128,11 +134,47 @@ const SelectDatasetRender = ({ inputs = [], item, nodeId }: RenderInputProps) =>
onOpenDatasetSelect,
selectedDatasets,
selectedDatasetsValue,
t,
theme.borders.base
t
]);
return Render;
};
});
export default React.memo(SelectDatasetRender);
export const SwitchAuthTmb = React.memo(function SwitchAuthTmb({
inputs = [],
item,
nodeId
}: RenderInputProps) {
const { t } = useTranslation();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const authTmbIdInput = useMemo(
() => inputs.find((v) => v.key === NodeInputKeyEnum.authTmbId),
[inputs]
);
return authTmbIdInput ? (
<Flex alignItems={'center'}>
<Box fontSize={'sm'}>{t('workflow:auth_tmb_id')}</Box>
<QuestionTip label={t('workflow:auth_tmb_id_tip')} />
<Switch
ml={1}
size={'sm'}
isChecked={!!authTmbIdInput.value}
onChange={(e) => {
onChangeNode({
nodeId,
key: NodeInputKeyEnum.authTmbId,
type: 'updateInput',
value: {
...authTmbIdInput,
value: e.target.checked
}
});
}}
/>
</Flex>
) : null;
});
export default SelectDatasetRender;

View File

@@ -9,6 +9,7 @@ import { AppContext } from '@/pages/app/detail/components/context';
import { getEditorVariables } from '../../../../../utils';
import { WorkflowNodeEdgeContext } from '../../../../../context/workflowInitContext';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import VariableTip from '@/components/common/Textarea/MyTextarea/VariableTip';
const TextareaRender = ({ item, nodeId }: RenderInputProps) => {
const { t } = useTranslation();
@@ -84,3 +85,10 @@ const TextareaRender = ({ item, nodeId }: RenderInputProps) => {
};
export default React.memo(TextareaRender);
export const TextareaRightComponent = React.memo(function TextareaRightComponent({
item,
nodeId
}: RenderInputProps) {
return <VariableTip transform={'translateY(2px)'} />;
});

View File

@@ -141,8 +141,6 @@ export const useChatTest = ({
chatId={chatId}
showMarkIcon
chatType="chat"
showRawSource
showNodeStatus
onStartChat={startChat}
/>
)

View File

@@ -34,8 +34,8 @@ import { postCopyApp } from '@/web/core/app/api/app';
import { formatTimeToChatTime } from '@fastgpt/global/common/string/time';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { useChatStore } from '@/web/core/chat/context/useChatStore';
import { useUserStore } from '@/web/support/user/useUserStore';
import { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
import UserBox from '@fastgpt/web/components/common/UserBox';
const HttpEditModal = dynamic(() => import('./HttpPluginEditModal'));
const ListItem = () => {
@@ -44,8 +44,6 @@ const ListItem = () => {
const { parentId = null } = router.query;
const { isPc } = useSystem();
const { loadAndGetTeamMembers } = useUserStore();
const { openConfirm: openMoveConfirm, ConfirmModal: MoveConfirmModal } = useConfirm({
type: 'common',
title: t('common:move.confirm'),
@@ -115,10 +113,6 @@ const ListItem = () => {
successToast: t('app:create_copy_success')
});
const { data: members = [] } = useRequest2(loadAndGetTeamMembers, {
manual: false
});
const { runAsync: onResumeInheritPermission } = useRequest2(
() => {
return resumeInheritPer(editPerApp!._id);
@@ -145,7 +139,6 @@ const ListItem = () => {
alignItems={'stretch'}
>
{myApps.map((app, index) => {
const owner = members.find((v) => v.tmbId === app.tmbId);
return (
<MyTooltip
key={app._id}
@@ -229,15 +222,12 @@ const ListItem = () => {
color={'myGray.500'}
>
<HStack spacing={3.5}>
{owner && (
<HStack spacing={1}>
<Avatar src={owner.avatar} w={'0.875rem'} borderRadius={'50%'} />
<Box maxW={'150px'} className="textEllipsis">
{owner.memberName}
</Box>
</HStack>
)}
<UserBox
sourceMember={app.sourceMember}
fontSize="xs"
avatarSize="1rem"
spacing={0.5}
/>
<PermissionIconText
private={app.private}
color={'myGray.500'}

View File

@@ -27,13 +27,11 @@ const ChatHeader = ({
history,
showHistory,
apps,
onRouteToAppDetail,
totalRecordsCount
}: {
history: ChatItemType[];
showHistory?: boolean;
apps?: AppListItemType[];
onRouteToAppDetail?: () => void;
totalRecordsCount: number;
}) => {
const { t } = useTranslation();
@@ -71,7 +69,7 @@ const ChatHeader = ({
)}
{/* control */}
{!isPlugin && <ToolMenu history={history} onRouteToAppDetail={onRouteToAppDetail} />}
{!isPlugin && <ToolMenu history={history} />}
</Flex>
);
};

View File

@@ -28,7 +28,6 @@ type HistoryItemType = {
const ChatHistorySlider = ({ confirmClearText }: { confirmClearText: string }) => {
const theme = useTheme();
const router = useRouter();
const isUserChatPage = router.pathname === '/chat';
const { t } = useTranslation();
@@ -46,6 +45,7 @@ const ChatHistorySlider = ({ confirmClearText }: { confirmClearText: string }) =
const appName = useContextSelector(ChatItemContext, (v) => v.chatBoxData?.app.name);
const appAvatar = useContextSelector(ChatItemContext, (v) => v.chatBoxData?.app.avatar);
const showRouteToAppDetail = useContextSelector(ChatItemContext, (v) => v.showRouteToAppDetail);
const concatHistory = useMemo(() => {
const formatHistories: HistoryItemType[] = histories.map((item) => {
@@ -77,8 +77,8 @@ const ChatHistorySlider = ({ confirmClearText }: { confirmClearText: string }) =
});
const canRouteToDetail = useMemo(
() => appId && userInfo?.team.permission.hasWritePer,
[appId, userInfo?.team.permission.hasWritePer]
() => appId && userInfo?.team.permission.hasWritePer && showRouteToAppDetail,
[appId, userInfo?.team.permission.hasWritePer, showRouteToAppDetail]
);
return (
@@ -287,7 +287,7 @@ const ChatHistorySlider = ({ confirmClearText }: { confirmClearText: string }) =
</ScrollData>
{/* exec */}
{!isPc && isUserChatPage && (
{!isPc && !!canRouteToDetail && (
<Flex
mt={2}
borderTop={theme.borders.base}

View File

@@ -14,6 +14,8 @@ import {
} from '@fastgpt/global/common/parentFolder/type';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import dynamic from 'next/dynamic';
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
import { useContextSelector } from 'use-context-selector';
const SelectOneResource = dynamic(() => import('@/components/common/folder/SelectOneResource'));
@@ -22,6 +24,8 @@ const SliderApps = ({ apps, activeAppId }: { apps: AppListItemType[]; activeAppI
const router = useRouter();
const isTeamChat = router.pathname === '/chat/team';
const showRouteToAppDetail = useContextSelector(ChatItemContext, (v) => v.showRouteToAppDetail);
const getAppList = useCallback(async ({ parentId }: GetResourceFolderListProps) => {
return getMyApps({
parentId,
@@ -50,34 +54,36 @@ const SliderApps = ({ apps, activeAppId }: { apps: AppListItemType[]; activeAppI
return (
<Flex flexDirection={'column'} h={'100%'}>
<Box mt={4} px={4}>
{!isTeamChat && (
<Flex
alignItems={'center'}
cursor={'pointer'}
py={2}
px={3}
borderRadius={'md'}
_hover={{ bg: 'myGray.200' }}
onClick={() => router.push('/app/list')}
>
<IconButton
mr={3}
icon={<MyIcon name={'common/backFill'} w={'1rem'} color={'primary.500'} />}
bg={'white'}
boxShadow={'1px 1px 9px rgba(0,0,0,0.15)'}
size={'smSquare'}
borderRadius={'50%'}
aria-label={''}
/>
{t('common:core.chat.Exit Chat')}
</Flex>
)}
</Box>
{showRouteToAppDetail && (
<>
<Box mt={4} px={4}>
<Flex
alignItems={'center'}
cursor={'pointer'}
py={2}
px={3}
borderRadius={'md'}
_hover={{ bg: 'myGray.200' }}
onClick={() => router.push('/app/list')}
>
<IconButton
mr={3}
icon={<MyIcon name={'common/backFill'} w={'1rem'} color={'primary.500'} />}
bg={'white'}
boxShadow={'1px 1px 9px rgba(0,0,0,0.15)'}
size={'smSquare'}
borderRadius={'50%'}
aria-label={''}
/>
{t('common:core.chat.Exit Chat')}
</Flex>
</Box>
<MyDivider h={2} my={1} />
</>
)}
{!isTeamChat && (
<>
<MyDivider h={2} my={1} />
<HStack
px={4}
my={2}

View File

@@ -7,20 +7,19 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import { useContextSelector } from 'use-context-selector';
import { ChatContext } from '@/web/core/chat/context/chatContext';
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
import { useRouter } from 'next/router';
const ToolMenu = ({
history,
onRouteToAppDetail
}: {
history: ChatItemType[];
onRouteToAppDetail?: () => void;
}) => {
const ToolMenu = ({ history }: { history: ChatItemType[] }) => {
const router = useRouter();
const { t } = useTranslation();
const { onExportChat } = useChatBox();
const onChangeChatId = useContextSelector(ChatContext, (v) => v.onChangeChatId);
const chatData = useContextSelector(ChatItemContext, (v) => v.chatBoxData);
const showRouteToAppDetail = useContextSelector(ChatItemContext, (v) => v.showRouteToAppDetail);
return history.length > 0 ? (
return (
<MyMenu
Button={
<IconButton
@@ -61,14 +60,14 @@ const ToolMenu = ({
// }
]
},
...(onRouteToAppDetail
...(showRouteToAppDetail
? [
{
children: [
{
icon: 'core/app/aiLight',
label: t('app:app_detail'),
onClick: onRouteToAppDetail
onClick: () => router.push(`/app/detail?appId=${chatData.appId}`)
}
]
}
@@ -76,8 +75,6 @@ const ToolMenu = ({
: [])
]}
/>
) : (
<Box w={'28px'} h={'28px'} />
);
};

View File

@@ -44,6 +44,7 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
const theme = useTheme();
const { t } = useTranslation();
const { isPc } = useSystem();
const { userInfo } = useUserStore();
const { setLastChatAppId, chatId, appId, outLinkAuthData } = useChatStore();
@@ -186,7 +187,6 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
apps={myApps}
history={chatRecords}
showHistory
onRouteToAppDetail={() => router.push(`/app/detail?appId=${appId}`)}
/>
{/* chat box */}
@@ -208,8 +208,6 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
feedbackType={'user'}
onStartChat={onStartChat}
chatType={'chat'}
showRawSource
showNodeStatus
isReady={!loading}
/>
)}
@@ -221,8 +219,8 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
);
};
const Render = (props: { appId: string }) => {
const { appId } = props;
const Render = (props: { appId: string; isStandalone?: string }) => {
const { appId, isStandalone } = props;
const { t } = useTranslation();
const { toast } = useToast();
const router = useRouter();
@@ -276,7 +274,12 @@ const Render = (props: { appId: string }) => {
return source === ChatSourceEnum.online ? (
<ChatContextProvider params={chatHistoryProviderParams}>
<ChatItemContextProvider>
<ChatItemContextProvider
showRouteToAppDetail={isStandalone !== '1'}
showRouteToDatasetDetail={isStandalone !== '1'}
isShowReadRawSource={true}
showNodeStatus
>
<ChatRecordContextProvider params={chatRecordProviderParams}>
<Chat myApps={myApps} />
</ChatRecordContextProvider>
@@ -289,6 +292,7 @@ export async function getServerSideProps(context: any) {
return {
props: {
appId: context?.query?.appId || '',
isStandalone: context?.query?.isStandalone || '',
...(await serviceSideProps(context, ['file', 'app', 'chat', 'workflow']))
}
};

View File

@@ -55,7 +55,6 @@ type Props = {
const OutLink = (props: Props) => {
const { t } = useTranslation();
const router = useRouter();
const { showRawSource, showNodeStatus } = props;
const {
shareId = '',
showHistory = '1',
@@ -287,8 +286,6 @@ const OutLink = (props: Props) => {
feedbackType={'user'}
onStartChat={startChat}
chatType="share"
showRawSource={showRawSource}
showNodeStatus={showNodeStatus}
/>
)}
</Box>
@@ -340,7 +337,12 @@ const Render = (props: Props) => {
return source === ChatSourceEnum.share ? (
<ChatContextProvider params={chatHistoryProviderParams}>
<ChatItemContextProvider>
<ChatItemContextProvider
showRouteToAppDetail={false}
showRouteToDatasetDetail={false}
isShowReadRawSource={props.showRawSource}
showNodeStatus={props.showNodeStatus}
>
<ChatRecordContextProvider params={chatRecordProviderParams}>
<OutLink {...props} />
</ChatRecordContextProvider>

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo } from 'react';
import NextHead from '@/components/common/NextHead';
import { getTeamChatInfo } from '@/web/core/chat/api';
import { useRouter } from 'next/router';
@@ -20,8 +20,7 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import ChatContextProvider, { ChatContext } from '@/web/core/chat/context/chatContext';
import { AppListItemType } from '@fastgpt/global/core/app/type';
import { useContextSelector } from 'use-context-selector';
import { InitChatResponse } from '@/global/core/chat/api';
import { defaultChatData, GetChatTypeEnum } from '@/global/core/chat/constants';
import { GetChatTypeEnum } from '@/global/core/chat/constants';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { getNanoid } from '@fastgpt/global/common/string/tools';
@@ -226,8 +225,6 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
feedbackType={'user'}
onStartChat={startChat}
chatType="team"
showRawSource
showNodeStatus
/>
)}
</Box>
@@ -299,7 +296,12 @@ const Render = (props: Props) => {
return source === ChatSourceEnum.team ? (
<ChatContextProvider params={contextParams}>
<ChatItemContextProvider>
<ChatItemContextProvider
showRouteToAppDetail={false}
showRouteToDatasetDetail={false}
isShowReadRawSource={true}
showNodeStatus
>
<ChatRecordContextProvider params={chatRecordProviderParams}>
<Chat {...props} myApps={myApps} />
</ChatRecordContextProvider>

View File

@@ -111,8 +111,7 @@ const CollectionPageContextProvider = ({ children }: { children: ReactNode }) =>
isLoading: isGetting,
pageNum,
pageSize
} = usePagination<DatasetCollectionsListItemType>({
api: getDatasetCollections,
} = usePagination(getDatasetCollections, {
pageSize: 20,
params: {
datasetId,

View File

@@ -49,7 +49,6 @@ const CustomAPIFileInput = () => {
parentId: '',
parentName: ''
});
const [parentUuid, setParentUuid] = useState<string>('');
const [paths, setPaths] = useState<ParentTreePathItemType[]>([]);
const [searchKey, setSearchKey] = useState('');
@@ -128,7 +127,7 @@ const CustomAPIFileInput = () => {
const handleItemClick = useCallback(
(item: APIFileItem) => {
if (item.type === 'folder') {
if (item.hasChild) {
setPaths((state) => [...state, { parentId: item.id, parentName: item.name }]);
return setParent({
parentId: item.id,
@@ -251,6 +250,7 @@ const CustomAPIFileInput = () => {
<Box fontSize={'sm'} fontWeight={'medium'} color={'myGray.900'}>
{item.name}
</Box>
{item.hasChild && <MyIcon name="core/chat/chevronRight" w={'18px'} ml={2} />}
</Flex>
);
})}

View File

@@ -28,10 +28,10 @@ import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { useFolderDrag } from '@/components/common/folder/useFolderDrag';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { useTranslation } from 'next-i18next';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import SideTag from './SideTag';
import { getModelProvider } from '@fastgpt/global/core/ai/provider';
import UserBox from '@fastgpt/web/components/common/UserBox';
const EditResourceModal = dynamic(() => import('@/components/common/Modal/EditResourceModal'));
@@ -39,7 +39,6 @@ function List() {
const { setLoading } = useSystemStore();
const { isPc } = useSystem();
const { t } = useTranslation();
const { loadAndGetTeamMembers } = useUserStore();
const {
loadMyDatasets,
setMoveDatasetId,
@@ -81,10 +80,6 @@ function List() {
}
});
const { data: members = [] } = useRequest2(loadAndGetTeamMembers, {
manual: false
});
const editPerDataset = useMemo(
() => (editPerDatasetIndex !== undefined ? myDatasets[editPerDatasetIndex] : undefined),
[editPerDatasetIndex, myDatasets]
@@ -156,7 +151,6 @@ function List() {
alignItems={'stretch'}
>
{formatDatasets.map((dataset, index) => {
const owner = members.find((v) => v.tmbId === dataset.tmbId);
const vectorModelAvatar = getModelProvider(dataset.vectorModel.provider)?.avatar;
return (
@@ -265,14 +259,12 @@ function List() {
color={'myGray.500'}
>
<HStack spacing={3.5}>
{owner && (
<HStack spacing={1}>
<Avatar src={owner.avatar} w={'0.875rem'} borderRadius={'50%'} />
<Box maxW={'150px'} className="textEllipsis" fontSize={'mini'}>
{owner.memberName}
</Box>
</HStack>
)}
<UserBox
sourceMember={dataset.sourceMember}
fontSize="xs"
avatarSize="1rem"
spacing={0.5}
/>
<PermissionIconText
flexShrink={0}
private={dataset.private}

View File

@@ -65,6 +65,10 @@ export const getScheduleTriggerApp = async () => {
teamId: String(app.teamId),
tmbId: String(app.tmbId)
},
runningUserInfo: {
teamId: String(app.teamId),
tmbId: String(app.tmbId)
},
uid: String(app.tmbId),
runtimeNodes: storeNodes2RuntimeNodes(nodes, getWorkflowEntryNodeIds(nodes)),
runtimeEdges: initWorkflowEdgeStatus(edges),

View File

@@ -13,6 +13,8 @@ import type { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/no
import type { ChatSchema } from '@fastgpt/global/core/chat/type';
import type { AppSchema } from '@fastgpt/global/core/app/type';
import { ChatModelType } from '@/constants/model';
import { TeamMemberStatusEnum } from '@fastgpt/global/support/user/team/constant';
import { SourceMember } from '@fastgpt/global/support/user/type';
export interface ShareAppItem {
_id: string;
@@ -45,4 +47,5 @@ export type AppLogsListItemType = {
markCount: number;
outLinkUid?: string;
tmbId: string;
sourceMember: SourceMember;
};

View File

@@ -10,15 +10,6 @@ import {
import { TrackEventName } from '@/web/common/system/constants';
import { SubPlanType } from '@fastgpt/global/support/wallet/sub/type';
export type PagingData<T> = {
pageNum: number;
pageSize: number;
data: T[];
total?: number;
};
export type RequestPaging = { pageNum: number; pageSize: number; [key]: any };
declare global {
var qaQueueLen: number;
var vectorQueueLen: number;

View File

@@ -5,7 +5,7 @@ import { AppUpdateParams, AppChangeOwnerBody } from '@/global/core/app/api';
import type { CreateAppBody } from '@/pages/api/core/app/create';
import type { ListAppBody } from '@/pages/api/core/app/list';
import { AppLogsListItemType } from '@/types/app';
import { PagingData } from '@/types';
import { PaginationResponse } from '@fastgpt/web/common/fetch/type';
/**
* 获取应用列表
@@ -39,7 +39,7 @@ export const putAppById = (id: string, data: AppUpdateParams) =>
// =================== chat logs
export const getAppChatLogs = (data: GetAppChatLogsParams) =>
POST<PagingData<AppLogsListItemType>>(`/core/app/getChatLogs`, data);
POST<PaginationResponse<AppLogsListItemType>>(`/core/app/getChatLogs`, data);
export const resumeInheritPer = (appId: string) =>
GET(`/core/app/resumeInheritPermission`, { appId });

View File

@@ -35,7 +35,8 @@ export const getTeamPlugTemplates = (data?: ListAppBody) =>
intro: app.intro,
showStatus: false,
version: app.pluginData?.nodeVersion || defaultNodeVersion,
isTool: true
isTool: true,
sourceMember: app.sourceMember
}))
);

View File

@@ -1,5 +1,5 @@
import { PostPublishAppProps } from '@/global/core/app/api';
import { GET, POST, DELETE, PUT } from '@/web/common/api/request';
import { GET, POST } from '@/web/common/api/request';
import type { AppVersionSchemaType } from '@fastgpt/global/core/app/version';
import { PaginationProps } from '@fastgpt/web/common/fetch/type';
import type {

View File

@@ -9,6 +9,12 @@ import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { AppChatConfigType, VariableItemType } from '@fastgpt/global/core/app/type';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
type ContextProps = {
showRouteToAppDetail: boolean;
showRouteToDatasetDetail: boolean;
isShowReadRawSource: boolean;
showNodeStatus: boolean;
};
type ChatBoxDataType = {
appId: string;
title?: string;
@@ -37,7 +43,7 @@ type ChatItemContextType = {
chatBoxData: ChatBoxDataType;
setChatBoxData: React.Dispatch<React.SetStateAction<ChatBoxDataType>>;
isPlugin: boolean;
};
} & ContextProps;
export const ChatItemContext = createContext<ChatItemContextType>({
ChatBoxRef: null,
@@ -61,7 +67,15 @@ export const ChatItemContext = createContext<ChatItemContextType>({
/*
Chat 对象的上下文
*/
const ChatItemContextProvider = ({ children }: { children: ReactNode }) => {
const ChatItemContextProvider = ({
children,
showRouteToAppDetail,
showRouteToDatasetDetail,
isShowReadRawSource,
showNodeStatus
}: {
children: ReactNode;
} & ContextProps) => {
const ChatBoxRef = useRef<ChatComponentRef>(null);
const variablesForm = useForm<ChatBoxInputFormType>();
@@ -113,16 +127,23 @@ const ChatItemContextProvider = ({ children }: { children: ReactNode }) => {
pluginRunTab,
setPluginRunTab,
resetVariables,
clearChatRecords
clearChatRecords,
showRouteToAppDetail,
showRouteToDatasetDetail,
isShowReadRawSource,
showNodeStatus
};
}, [
chatBoxData,
setChatBoxData,
clearChatRecords,
isPlugin,
variablesForm,
pluginRunTab,
resetVariables,
variablesForm
clearChatRecords,
showRouteToAppDetail,
showRouteToDatasetDetail,
isShowReadRawSource,
showNodeStatus
]);
return <ChatItemContext.Provider value={contextValue}>{children}</ChatItemContext.Provider>;

View File

@@ -2,7 +2,7 @@ import { getPaginationRecordsBody } from '@/pages/api/core/chat/getPaginationRec
import { ChatSiteItemType } from '@fastgpt/global/core/chat/type';
import { PaginationResponse } from '@fastgpt/web/common/fetch/type';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import React, { ReactNode, useEffect, useMemo, useState } from 'react';
import React, { ReactNode, useMemo, useState } from 'react';
import { createContext, useContextSelector } from 'use-context-selector';
import { ChatItemContext } from './chatItemContext';
import { getChatRecords } from '../api';
@@ -68,7 +68,7 @@ const ChatRecordContextProvider = ({
const res = await getChatRecords(data);
// First load scroll to bottom
if (data.offset === 0) {
if (Number(data.offset) === 0) {
function scrollToBottom() {
requestAnimationFrame(
ChatBoxRef?.current ? () => ChatBoxRef?.current?.scrollToBottom?.() : scrollToBottom

View File

@@ -37,7 +37,6 @@ import type { DatasetCollectionItemType } from '@fastgpt/global/core/dataset/typ
import { DatasetCollectionSyncResultEnum } from '@fastgpt/global/core/dataset/constants';
import type { DatasetDataItemType } from '@fastgpt/global/core/dataset/type';
import type { DatasetCollectionsListItemType } from '@/global/core/dataset/type.d';
import { PagingData } from '@/types';
import type { getDatasetTrainingQueueResponse } from '@/pages/api/core/dataset/training/getDatasetTrainingQueue';
import type { rebuildEmbeddingBody } from '@/pages/api/core/dataset/training/rebuildEmbedding';
import type {
@@ -66,8 +65,6 @@ import type {
listExistIdQuery,
listExistIdResponse
} from '@/pages/api/core/dataset/apiDataset/listExistId';
import { FeishuServer, YuqueServer } from '@fastgpt/global/core/dataset/apiDataset';
import { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
/* ======================== dataset ======================= */
export const getDatasets = (data: GetDatasetListBody) =>
@@ -110,7 +107,7 @@ export const postSearchText = (data: SearchTestProps) =>
/* ============================= collections ==================================== */
export const getDatasetCollections = (data: GetDatasetCollectionsProps) =>
POST<PagingData<DatasetCollectionsListItemType>>(`/core/dataset/collection/list`, data);
POST<PaginationResponse<DatasetCollectionsListItemType>>(`/core/dataset/collection/listV2`, data);
export const getDatasetCollectionPathById = (parentId: string) =>
GET<ParentTreePathItemType[]>(`/core/dataset/collection/paths`, { parentId });
export const getDatasetCollectionById = (id: string) =>

View File

@@ -1,7 +1,7 @@
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyModal from '@fastgpt/web/components/common/MyModal';
import ParentPaths from '@/components/common/ParentPaths';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { useRequest, useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { getDatasetCollectionPathById, getDatasetCollections } from '@/web/core/dataset/api';
import { Box, Flex, ModalFooter, Button, useTheme, Grid, Card, ModalBody } from '@chakra-ui/react';
import { DatasetCollectionTypeEnum } from '@fastgpt/global/core/dataset/constants';
@@ -48,20 +48,24 @@ const SelectCollections = ({
useQuery(['loadDatasetDetail', datasetId], () => loadDatasetDetail(datasetId));
const { data, isLoading } = useQuery(['getDatasetCollections', parentId], () =>
getDatasetCollections({
datasetId,
parentId,
selectFolder: type === 'folder',
simple: true,
pageNum: 1,
pageSize: 50
})
const { data, loading: isLoading } = useRequest2(
() =>
getDatasetCollections({
datasetId,
parentId,
selectFolder: type === 'folder',
simple: true,
pageNum: 1,
pageSize: 50
}),
{
manual: false,
refreshDeps: [datasetId, parentId, type]
}
);
const formatCollections = useMemo(
() =>
data?.data.map((collection) => {
data?.list.map((collection) => {
const icon = getCollectionIcon(collection.type, collection.name);
return {
@@ -111,7 +115,7 @@ const SelectCollections = ({
title={
<Box>
<ParentPaths
paths={paths.map((path, i) => ({
paths={paths.map((path) => ({
parentId: path.parentId,
parentName: path.parentName
}))}

View File

@@ -118,7 +118,7 @@ export const storeNode2FlowNode = ({
toolDescription: t(templateInput.toolDescription ?? (storeInput.toolDescription as any)),
selectedTypeIndex: storeInput.selectedTypeIndex ?? templateInput.selectedTypeIndex,
value: storeInput.value ?? templateInput.value,
value: storeInput.value,
valueType: storeInput.valueType ?? templateInput.valueType,
label: storeInput.label ?? templateInput.label
};

View File

@@ -1,6 +1,6 @@
import { GET, POST, PUT } from '@/web/common/api/request';
import { GET, POST } from '@/web/common/api/request';
import type { PromotionRecordType } from '@/global/support/api/userRes.d';
import { PagingData, type RequestPaging } from '@/types';
import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type';
/* get promotion init data */
export const getPromotionInitData = () =>
@@ -10,5 +10,8 @@ export const getPromotionInitData = () =>
}>('/proApi/support/activity/promotion/getPromotionData');
/* promotion records */
export const getPromotionRecords = (data: RequestPaging) =>
POST<PagingData<PromotionRecordType>>(`/proApi/support/activity/promotion/getPromotions`, data);
export const getPromotionRecords = (data: PaginationProps) =>
POST<PaginationResponse<PromotionRecordType>>(
`/proApi/support/activity/promotion/getPromotions`,
data
);

View File

@@ -90,3 +90,5 @@ export const getCaptchaPic = (username: string) =>
GET<{
captchaImage: string;
}>('/proApi/support/user/account/captcha/getImgCaptcha', { username });
export const postSyncMembers = () => POST('/proApi/support/user/team/org/sync');

View File

@@ -1,10 +1,10 @@
import { GET, POST, PUT } from '@/web/common/api/request';
import type { PagingData, RequestPaging } from '@/types';
import { GET, POST } from '@/web/common/api/request';
import type { UserInformSchema } from '@fastgpt/global/support/user/inform/type';
import { SystemMsgModalValueType } from '@fastgpt/service/support/user/inform/type';
import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type';
export const getInforms = (data: RequestPaging) =>
POST<PagingData<UserInformSchema>>(`/proApi/support/user/inform/list`, data);
export const getInforms = (data: PaginationProps) =>
POST<PaginationResponse<UserInformSchema>>(`/proApi/support/user/inform/list`, data);
export const getUnreadCount = () =>
GET<{

View File

@@ -19,6 +19,7 @@ import {
} from '@fastgpt/global/support/user/team/type.d';
import { FeTeamPlanStatusType, TeamSubSchema } from '@fastgpt/global/support/wallet/sub/type';
import { TeamInvoiceHeaderType } from '@fastgpt/global/support/user/team/type';
import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type';
/* --------------- team ---------------- */
export const getTeamList = (status: `${TeamMemberSchema['status']}`) =>
@@ -30,8 +31,8 @@ export const putSwitchTeam = (teamId: string) =>
PUT<string>(`/proApi/support/user/team/switch`, { teamId });
/* --------------- team member ---------------- */
export const getTeamMembers = () =>
GET<TeamMemberItemType[]>(`/proApi/support/user/team/member/list`);
export const getTeamMembers = (props: PaginationProps) =>
GET<PaginationResponse<TeamMemberItemType>>(`/proApi/support/user/team/member/list`, props);
export const postInviteTeamMember = (data: InviteMemberProps) =>
POST<InviteMemberResponse>(`/proApi/support/user/team/member/invite`, data);
export const putUpdateMemberName = (name: string) =>

View File

@@ -30,9 +30,6 @@ type State = {
teamPlanStatus: FeTeamPlanStatusType | null;
initTeamPlanStatus: () => Promise<any>;
teamMembers: TeamMemberItemType[];
loadAndGetTeamMembers: (init?: boolean) => Promise<TeamMemberItemType[]>;
teamMemberGroups: MemberGroupListType;
myGroups: MemberGroupListType;
loadAndGetGroups: (init?: boolean) => Promise<MemberGroupListType>;
@@ -102,7 +99,7 @@ export const useUserStore = create<State>()(
},
// team
teamPlanStatus: null,
initTeamPlanStatus() {
async initTeamPlanStatus() {
return getTeamPlanStatus().then((res) => {
set((state) => {
state.teamPlanStatus = res;
@@ -110,21 +107,6 @@ export const useUserStore = create<State>()(
return res;
});
},
teamMembers: [],
loadAndGetTeamMembers: async (init = false) => {
if (!useSystemStore.getState()?.feConfigs?.isPlus) return [];
const randomRefresh = Math.random() > 0.7;
if (!randomRefresh && !init && get().teamMembers?.length)
return Promise.resolve(get().teamMembers);
const res = await getTeamMembers();
set((state) => {
state.teamMembers = res;
});
return res;
},
teamMemberGroups: [],
teamOrgs: [],
myGroups: [],

View File

@@ -1,14 +1,14 @@
import { PagingData, RequestPaging } from '@/types';
import { GET, POST } from '@/web/common/api/request';
import { CreateBillProps, CreateBillResponse } from '@fastgpt/global/support/wallet/bill/api';
import { BillTypeEnum } from '@fastgpt/global/support/wallet/bill/constants';
import type { BillSchemaType } from '@fastgpt/global/support/wallet/bill/type.d';
import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type';
export const getBills = (
data: RequestPaging & {
data: PaginationProps<{
type?: BillTypeEnum;
}
) => POST<PagingData<BillSchemaType>>(`/proApi/support/wallet/bill/list`, data);
}>
) => POST<PaginationResponse<BillSchemaType>>(`/proApi/support/wallet/bill/list`, data);
export const getWxPayQRCode = (data: CreateBillProps) =>
POST<CreateBillResponse>(`/proApi/support/wallet/bill/create`, data);

View File

@@ -1,8 +1,8 @@
import { PagingData, RequestPaging } from '@/types';
import { GET, POST } from '@/web/common/api/request';
import { BillTypeEnum } from '@fastgpt/global/support/wallet/bill/constants';
import { InvoiceType } from '@fastgpt/global/support/wallet/bill/type';
import { InvoiceSchemaType } from '@fastgpt/global/support/wallet/bill/type';
import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type';
export type invoiceBillDataType = {
type: BillTypeEnum;
price: number;
@@ -16,5 +16,5 @@ export const getInvoiceBillsList = () =>
export const submitInvoice = (data: InvoiceType) =>
POST(`/proApi/support/wallet/bill/invoice/submit`, data);
export const getInvoiceRecords = (data: RequestPaging) =>
POST<PagingData<InvoiceSchemaType>>(`/proApi/support/wallet/bill/invoice/records`, data);
export const getInvoiceRecords = (data: PaginationProps) =>
POST<PaginationResponse<InvoiceSchemaType>>(`/proApi/support/wallet/bill/invoice/records`, data);

View File

@@ -1,10 +1,17 @@
import { GET, POST } from '@/web/common/api/request';
import { POST } from '@/web/common/api/request';
import { CreateTrainingUsageProps } from '@fastgpt/global/support/wallet/usage/api.d';
import type { PagingData, RequestPaging } from '@/types';
import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants';
import type { UsageItemType } from '@fastgpt/global/support/wallet/usage/type';
import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type';
export const getUserUsages = (data: RequestPaging) =>
POST<PagingData<UsageItemType>>(`/proApi/support/wallet/usage/getUsage`, data);
export const getUserUsages = (
data: PaginationProps<{
dateStart: Date;
dateEnd: Date;
source?: UsageSourceEnum;
teamMemberId: string;
}>
) => POST<PaginationResponse<UsageItemType>>(`/proApi/support/wallet/usage/getUsage`, data);
export const postCreateTrainingUsage = (data: CreateTrainingUsageProps) =>
POST<string>(`/support/wallet/usage/createTrainingUsage`, data);