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
This commit is contained in:
@@ -40,7 +40,7 @@ export type FastGPTConfigFileType = {
|
||||
|
||||
export type FastGPTFeConfigsType = {
|
||||
show_emptyChat?: boolean;
|
||||
register_method?: ['email' | 'phone'];
|
||||
register_method?: ['email' | 'phone' | 'sync'];
|
||||
login_method?: ['email' | 'phone']; // Attention: login method is diffrent with oauth
|
||||
find_password_method?: ['email' | 'phone'];
|
||||
bind_notification_method?: ['email' | 'phone'];
|
||||
@@ -76,7 +76,6 @@ export type FastGPTFeConfigsType = {
|
||||
wecom?: {
|
||||
corpid?: string;
|
||||
agentid?: string;
|
||||
secret?: string;
|
||||
};
|
||||
microsoft?: {
|
||||
clientId?: string;
|
||||
|
||||
4
packages/global/core/app/type.d.ts
vendored
4
packages/global/core/app/type.d.ts
vendored
@@ -12,8 +12,9 @@ import { TeamTagSchema as TeamTagsSchemaType } from '@fastgpt/global/support/use
|
||||
import { StoreEdgeItemType } from '../workflow/type/edge';
|
||||
import { AppPermission } from '../../support/permission/app/controller';
|
||||
import { ParentIdType } from '../../common/parentFolder/type';
|
||||
import { FlowNodeInputTypeEnum } from 'core/workflow/node/constant';
|
||||
import { FlowNodeInputTypeEnum } from '../../core/workflow/node/constant';
|
||||
import { WorkflowTemplateBasicType } from '@fastgpt/global/core/workflow/type';
|
||||
import { SourceMemberType } from '../../support/user/type';
|
||||
|
||||
export type AppSchema = {
|
||||
_id: string;
|
||||
@@ -63,6 +64,7 @@ export type AppListItemType = {
|
||||
permission: AppPermission;
|
||||
inheritPermission?: boolean;
|
||||
private?: boolean;
|
||||
sourceMember: SourceMemberType;
|
||||
};
|
||||
|
||||
export type AppDetailType = AppSchema & {
|
||||
|
||||
3
packages/global/core/app/version.d.ts
vendored
3
packages/global/core/app/version.d.ts
vendored
@@ -1,5 +1,7 @@
|
||||
import { TeamMemberStatusEnum } from 'support/user/team/constant';
|
||||
import { StoreEdgeItemType } from '../workflow/type/edge';
|
||||
import { AppChatConfigType, AppSchema } from './type';
|
||||
import { SourceMemberType } from 'support/user/type';
|
||||
|
||||
export type AppVersionSchemaType = {
|
||||
_id: string;
|
||||
@@ -20,4 +22,5 @@ export type VersionListItemType = {
|
||||
time: Date;
|
||||
isPublish: boolean | undefined;
|
||||
tmbId: string;
|
||||
sourceMember: SourceMemberType;
|
||||
};
|
||||
|
||||
2
packages/global/core/dataset/type.d.ts
vendored
2
packages/global/core/dataset/type.d.ts
vendored
@@ -11,6 +11,7 @@ import {
|
||||
import { DatasetPermission } from '../../support/permission/dataset/controller';
|
||||
import { Permission } from '../../support/permission/controller';
|
||||
import { APIFileServer, FeishuServer, YuqueServer } from './apiDataset';
|
||||
import { SourceMemberType } from 'support/user/type';
|
||||
|
||||
export type DatasetSchemaType = {
|
||||
_id: string;
|
||||
@@ -165,6 +166,7 @@ export type DatasetListItemType = {
|
||||
vectorModel: VectorModelItemType;
|
||||
inheritPermission: boolean;
|
||||
private?: boolean;
|
||||
sourceMember?: SourceMemberType;
|
||||
};
|
||||
|
||||
export type DatasetItemType = Omit<DatasetSchemaType, 'vectorModel' | 'agentModel'> & {
|
||||
|
||||
1
packages/global/core/workflow/type/node.d.ts
vendored
1
packages/global/core/workflow/type/node.d.ts
vendored
@@ -89,6 +89,7 @@ export type NodeTemplateListItemType = {
|
||||
hasTokenFee?: boolean; // 是否配置积分
|
||||
instructions?: string; // 使用说明
|
||||
courseUrl?: string; // 教程链接
|
||||
sourceMember?: SourceMember;
|
||||
};
|
||||
|
||||
export type NodeTemplateListType = {
|
||||
|
||||
@@ -12,6 +12,7 @@ export type CreateTeamProps = {
|
||||
avatar?: string;
|
||||
defaultTeam?: boolean;
|
||||
memberName?: string;
|
||||
memberAvatar?: string;
|
||||
};
|
||||
export type UpdateTeamProps = Omit<ThirdPartyAccountType, 'externalWorkflowVariable'> & {
|
||||
name?: string;
|
||||
|
||||
@@ -5,8 +5,6 @@ export const OrgMemberCollectionName = 'team_org_members';
|
||||
|
||||
export const getOrgChildrenPath = (org: OrgSchemaType) => `${org.path}/${org.pathId}`;
|
||||
|
||||
// export enum OrgMemberRole {
|
||||
// owner = 'owner',
|
||||
// admin = 'admin',
|
||||
// member = 'member'
|
||||
// }
|
||||
export enum SyncOrgSourceEnum {
|
||||
wecom = 'wecom'
|
||||
}
|
||||
|
||||
1
packages/global/support/user/team/type.d.ts
vendored
1
packages/global/support/user/team/type.d.ts
vendored
@@ -44,6 +44,7 @@ export type TeamMemberSchema = {
|
||||
name: string;
|
||||
role: `${TeamMemberRoleEnum}`;
|
||||
status: `${TeamMemberStatusEnum}`;
|
||||
avatar: string;
|
||||
defaultTeam: boolean;
|
||||
};
|
||||
|
||||
|
||||
10
packages/global/support/user/type.d.ts
vendored
10
packages/global/support/user/type.d.ts
vendored
@@ -1,12 +1,12 @@
|
||||
import { TeamPermission } from '../permission/user/controller';
|
||||
import { UserStatusEnum } from './constant';
|
||||
import { TeamMemberStatusEnum } from './team/constant';
|
||||
import { TeamTmbItemType } from './team/type';
|
||||
|
||||
export type UserModelSchema = {
|
||||
_id: string;
|
||||
username: string;
|
||||
password: string;
|
||||
avatar: string;
|
||||
promotionRate: number;
|
||||
inviterId?: string;
|
||||
openaiKey: string;
|
||||
@@ -22,7 +22,7 @@ export type UserModelSchema = {
|
||||
export type UserType = {
|
||||
_id: string;
|
||||
username: string;
|
||||
avatar: string;
|
||||
avatar: string; // it should be team member's avatar after 4.8.18
|
||||
timezone: string;
|
||||
promotionRate: UserModelSchema['promotionRate'];
|
||||
team: TeamTmbItemType;
|
||||
@@ -30,3 +30,9 @@ export type UserType = {
|
||||
notificationAccount?: string;
|
||||
permission: TeamPermission;
|
||||
};
|
||||
|
||||
export type SourceMemberType = {
|
||||
name: string;
|
||||
avatar: string;
|
||||
status: `${TeamMemberStatusEnum}`;
|
||||
};
|
||||
|
||||
21
packages/service/common/api/pagination.ts
Normal file
21
packages/service/common/api/pagination.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
|
||||
import { ApiRequestProps } from '../../type/next';
|
||||
|
||||
export function parsePaginationRequest(req: ApiRequestProps) {
|
||||
const {
|
||||
pageSize = 10,
|
||||
pageNum = 1,
|
||||
offset = 0
|
||||
} = Object.keys(req.body).includes('pageSize')
|
||||
? req.body
|
||||
: Object.keys(req.query).includes('pageSize')
|
||||
? req.query
|
||||
: {};
|
||||
if (!pageSize || (pageNum === undefined && offset === undefined)) {
|
||||
throw new Error(CommonErrEnum.missingParams);
|
||||
}
|
||||
return {
|
||||
pageSize: Number(pageSize),
|
||||
offset: offset ? Number(offset) : (Number(pageNum) - 1) * Number(pageSize)
|
||||
};
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import type { NextApiResponse, NextApiRequest } from 'next';
|
||||
import NextCors from 'nextjs-cors';
|
||||
|
||||
export async function withNextCors(req: NextApiRequest, res: NextApiResponse) {
|
||||
const methods = ['GET', 'eHEAD', 'PUT', 'PATCH', 'POST', 'DELETE'];
|
||||
const methods = ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE'];
|
||||
|
||||
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',');
|
||||
const origin = req.headers.origin;
|
||||
|
||||
@@ -17,7 +17,6 @@ import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
|
||||
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
|
||||
import { MemberGroupSchemaType } from '@fastgpt/global/support/permission/memberGroup/type';
|
||||
import { TeamMemberSchema } from '@fastgpt/global/support/user/team/type';
|
||||
import { UserModelSchema } from '@fastgpt/global/support/user/type';
|
||||
import { OrgSchemaType } from '@fastgpt/global/support/user/team/org/type';
|
||||
import { getOrgIdSetWithParentByTmbId } from './org/controllers';
|
||||
|
||||
@@ -151,13 +150,9 @@ export const getClbsAndGroupsWithInfo = async ({
|
||||
$exists: true
|
||||
}
|
||||
})
|
||||
.populate<{ tmb: TeamMemberSchema & { user: UserModelSchema } }>({
|
||||
.populate<{ tmb: TeamMemberSchema }>({
|
||||
path: 'tmb',
|
||||
select: 'name userId role',
|
||||
populate: {
|
||||
path: 'user',
|
||||
select: 'avatar'
|
||||
}
|
||||
select: 'name userId avatar'
|
||||
})
|
||||
.lean(),
|
||||
MongoResourcePermission.find({
|
||||
|
||||
@@ -41,7 +41,7 @@ export async function getUserDetail({
|
||||
return {
|
||||
_id: user._id,
|
||||
username: user.username,
|
||||
avatar: user.avatar,
|
||||
avatar: tmb.avatar,
|
||||
timezone: user.timezone,
|
||||
promotionRate: user.promotionRate,
|
||||
team: tmb,
|
||||
|
||||
@@ -3,7 +3,6 @@ const { Schema } = connectionMongo;
|
||||
import { hashStr } from '@fastgpt/global/common/string/tools';
|
||||
import type { UserModelSchema } from '@fastgpt/global/support/user/type';
|
||||
import { UserStatusEnum, userStatusMap } from '@fastgpt/global/support/user/constant';
|
||||
import { getRandomUserAvatar } from '@fastgpt/global/support/user/utils';
|
||||
|
||||
export const userCollectionName = 'users';
|
||||
|
||||
@@ -33,11 +32,6 @@ const UserSchema = new Schema({
|
||||
type: Date,
|
||||
default: () => new Date()
|
||||
},
|
||||
avatar: {
|
||||
type: String,
|
||||
default: () => getRandomUserAvatar()
|
||||
},
|
||||
|
||||
promotionRate: {
|
||||
type: Number,
|
||||
default: 15
|
||||
@@ -62,7 +56,10 @@ const UserSchema = new Schema({
|
||||
ref: userCollectionName
|
||||
},
|
||||
fastgpt_sem: Object,
|
||||
sourceDomain: String
|
||||
sourceDomain: String,
|
||||
|
||||
/** @deprecated */
|
||||
avatar: String
|
||||
});
|
||||
|
||||
try {
|
||||
|
||||
@@ -37,7 +37,7 @@ async function getTeamMember(match: Record<string, any>): Promise<TeamTmbItemTyp
|
||||
teamAvatar: tmb.team.avatar,
|
||||
teamName: tmb.team.name,
|
||||
memberName: tmb.name,
|
||||
avatar: tmb.team.avatar,
|
||||
avatar: tmb.avatar,
|
||||
balance: tmb.team.balance,
|
||||
tmbId: String(tmb._id),
|
||||
teamDomain: tmb.team?.teamDomain,
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
TeamMemberCollectionName,
|
||||
TeamCollectionName
|
||||
} from '@fastgpt/global/support/user/team/constant';
|
||||
import { getRandomUserAvatar } from '@fastgpt/global/support/user/utils';
|
||||
|
||||
const TeamMemberSchema = new Schema({
|
||||
teamId: {
|
||||
@@ -35,11 +36,14 @@ const TeamMemberSchema = new Schema({
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
avatar: {
|
||||
type: String,
|
||||
default: getRandomUserAvatar()
|
||||
},
|
||||
|
||||
// Abandoned
|
||||
role: {
|
||||
type: String
|
||||
// enum: Object.keys(TeamMemberRoleMap) // disable enum validation for old data
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
15
packages/web/common/fetch/type.d.ts
vendored
15
packages/web/common/fetch/type.d.ts
vendored
@@ -1,8 +1,13 @@
|
||||
export type PaginationProps<T = {}> = T & {
|
||||
offset: number;
|
||||
pageSize: number;
|
||||
};
|
||||
export type PaginationResponse<T = any> = {
|
||||
import { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
|
||||
|
||||
type PaginationProps<T = {}> = T & {
|
||||
pageSize: number | string;
|
||||
} & RequireOnlyOne<{
|
||||
offset: number | string;
|
||||
pageNum: number | string;
|
||||
}>;
|
||||
|
||||
type PaginationResponse<T = {}> = {
|
||||
total: number;
|
||||
list: T[];
|
||||
};
|
||||
|
||||
@@ -21,7 +21,15 @@ import type { ButtonProps, MenuItemProps } from '@chakra-ui/react';
|
||||
import MyIcon from '../Icon';
|
||||
import { useRequest2 } from '../../../hooks/useRequest';
|
||||
import MyDivider from '../MyDivider';
|
||||
import { useScrollPagination } from '../../../hooks/useScrollPagination';
|
||||
|
||||
/** 选择组件 Props 类型
|
||||
* value: 选中的值
|
||||
* placeholder: 占位符
|
||||
* list: 列表数据
|
||||
* isLoading: 是否加载中
|
||||
* ScrollData: 分页滚动数据控制器 [useScrollPagination] 的返回值
|
||||
* */
|
||||
export type SelectProps<T = any> = ButtonProps & {
|
||||
value?: T;
|
||||
placeholder?: string;
|
||||
@@ -34,6 +42,7 @@ export type SelectProps<T = any> = ButtonProps & {
|
||||
}[];
|
||||
isLoading?: boolean;
|
||||
onchange?: (val: T) => any | Promise<any>;
|
||||
ScrollData?: ReturnType<typeof useScrollPagination>['ScrollData'];
|
||||
};
|
||||
|
||||
const MySelect = <T = any,>(
|
||||
@@ -44,6 +53,7 @@ const MySelect = <T = any,>(
|
||||
list = [],
|
||||
onchange,
|
||||
isLoading = false,
|
||||
ScrollData,
|
||||
...props
|
||||
}: SelectProps<T>,
|
||||
ref: ForwardedRef<{
|
||||
@@ -154,39 +164,45 @@ const MySelect = <T = any,>(
|
||||
maxH={'40vh'}
|
||||
overflowY={'auto'}
|
||||
>
|
||||
{list.map((item, i) => (
|
||||
<Box key={i}>
|
||||
<MenuItem
|
||||
{...menuItemStyles}
|
||||
{...(value === item.value
|
||||
? {
|
||||
ref: SelectedItemRef,
|
||||
color: 'primary.700',
|
||||
bg: 'myGray.100',
|
||||
fontWeight: '600'
|
||||
{(() => {
|
||||
const component = list.map((item, i) => (
|
||||
<Box key={i}>
|
||||
<MenuItem
|
||||
{...menuItemStyles}
|
||||
{...(value === item.value
|
||||
? {
|
||||
ref: SelectedItemRef,
|
||||
color: 'primary.700',
|
||||
bg: 'myGray.100',
|
||||
fontWeight: '600'
|
||||
}
|
||||
: {
|
||||
color: 'myGray.900'
|
||||
})}
|
||||
onClick={() => {
|
||||
if (onChange && value !== item.value) {
|
||||
onChange(item.value);
|
||||
}
|
||||
: {
|
||||
color: 'myGray.900'
|
||||
})}
|
||||
onClick={() => {
|
||||
if (onChange && value !== item.value) {
|
||||
onChange(item.value);
|
||||
}
|
||||
}}
|
||||
whiteSpace={'pre-wrap'}
|
||||
fontSize={'sm'}
|
||||
display={'block'}
|
||||
>
|
||||
<Box>{item.label}</Box>
|
||||
{item.description && (
|
||||
<Box color={'myGray.500'} fontSize={'xs'}>
|
||||
{item.description}
|
||||
</Box>
|
||||
)}
|
||||
</MenuItem>
|
||||
{item.showBorder && <MyDivider my={2} />}
|
||||
</Box>
|
||||
))}
|
||||
}}
|
||||
whiteSpace={'pre-wrap'}
|
||||
fontSize={'sm'}
|
||||
display={'block'}
|
||||
>
|
||||
<Box>{item.label}</Box>
|
||||
{item.description && (
|
||||
<Box color={'myGray.500'} fontSize={'xs'}>
|
||||
{item.description}
|
||||
</Box>
|
||||
)}
|
||||
</MenuItem>
|
||||
{item.showBorder && <MyDivider my={2} />}
|
||||
</Box>
|
||||
));
|
||||
if (ScrollData) {
|
||||
return <ScrollData>{component}</ScrollData>;
|
||||
}
|
||||
return component;
|
||||
})()}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</Box>
|
||||
|
||||
23
packages/web/components/common/UserBox/index.tsx
Normal file
23
packages/web/components/common/UserBox/index.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Box, HStack, type StackProps } from '@chakra-ui/react';
|
||||
import { SourceMemberType } from '@fastgpt/global/support/user/type';
|
||||
import React from 'react';
|
||||
import Avatar from '../Avatar';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import Tag from '../Tag';
|
||||
|
||||
export type UserBoxProps = {
|
||||
sourceMember: SourceMemberType;
|
||||
avatarSize?: string;
|
||||
} & StackProps;
|
||||
function UserBox({ sourceMember, avatarSize = '1.25rem', ...props }: UserBoxProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<HStack space="1" {...props}>
|
||||
<Avatar src={sourceMember.avatar} w={avatarSize} />
|
||||
<Box>{sourceMember.name}</Box>
|
||||
{sourceMember.status === 'leave' && <Tag color="gray">{t('account_team:leaved')}</Tag>}
|
||||
</HStack>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(UserBox);
|
||||
@@ -4,7 +4,6 @@ import { ArrowBackIcon, ArrowForwardIcon } from '@chakra-ui/icons';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useToast } from './useToast';
|
||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
|
||||
import {
|
||||
useBoolean,
|
||||
useLockFn,
|
||||
@@ -14,37 +13,33 @@ import {
|
||||
useThrottleEffect
|
||||
} from 'ahooks';
|
||||
|
||||
import { PaginationProps, PaginationResponse } from '../common/fetch/type';
|
||||
|
||||
const thresholdVal = 200;
|
||||
|
||||
type PagingData<T> = {
|
||||
pageNum: number;
|
||||
pageSize: number;
|
||||
data: T[];
|
||||
total?: number;
|
||||
};
|
||||
|
||||
export function usePagination<ResT = any>({
|
||||
api,
|
||||
pageSize = 10,
|
||||
params = {},
|
||||
defaultRequest = true,
|
||||
type = 'button',
|
||||
onChange,
|
||||
refreshDeps,
|
||||
scrollLoadType = 'bottom',
|
||||
EmptyTip
|
||||
}: {
|
||||
api: (data: any) => Promise<PagingData<ResT>>;
|
||||
pageSize?: number;
|
||||
params?: Record<string, any>;
|
||||
defaultRequest?: boolean;
|
||||
type?: 'button' | 'scroll';
|
||||
onChange?: (pageNum: number) => void;
|
||||
refreshDeps?: any[];
|
||||
throttleWait?: number;
|
||||
scrollLoadType?: 'top' | 'bottom';
|
||||
EmptyTip?: React.JSX.Element;
|
||||
}) {
|
||||
export function usePagination<DataT, ResT = {}>(
|
||||
api: (data: PaginationProps<DataT>) => Promise<PaginationResponse<ResT>>,
|
||||
{
|
||||
pageSize = 10,
|
||||
params,
|
||||
defaultRequest = true,
|
||||
type = 'button',
|
||||
onChange,
|
||||
refreshDeps,
|
||||
scrollLoadType = 'bottom',
|
||||
EmptyTip
|
||||
}: {
|
||||
pageSize?: number;
|
||||
params?: DataT;
|
||||
defaultRequest?: boolean;
|
||||
type?: 'button' | 'scroll';
|
||||
onChange?: (pageNum: number) => void;
|
||||
refreshDeps?: any[];
|
||||
throttleWait?: number;
|
||||
scrollLoadType?: 'top' | 'bottom';
|
||||
EmptyTip?: React.JSX.Element;
|
||||
}
|
||||
) {
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -64,7 +59,7 @@ export function usePagination<ResT = any>({
|
||||
setTrue();
|
||||
|
||||
try {
|
||||
const res: PagingData<ResT> = await api({
|
||||
const res = await api({
|
||||
pageNum: num,
|
||||
pageSize,
|
||||
...params
|
||||
@@ -93,13 +88,13 @@ export function usePagination<ResT = any>({
|
||||
);
|
||||
}
|
||||
|
||||
setData((prevData) => (num === 1 ? res.data : [...res.data, ...prevData]));
|
||||
setData((prevData) => (num === 1 ? res.list : [...res.list, ...prevData]));
|
||||
adjustScrollPosition();
|
||||
} else {
|
||||
setData((prevData) => (num === 1 ? res.data : [...prevData, ...res.data]));
|
||||
setData((prevData) => (num === 1 ? res.list : [...prevData, ...res.list]));
|
||||
}
|
||||
} else {
|
||||
setData(res.data);
|
||||
setData(res.list);
|
||||
}
|
||||
|
||||
onChange?.(num);
|
||||
|
||||
@@ -35,5 +35,9 @@
|
||||
"user_team_invite_member": "邀请成员",
|
||||
"user_team_leave_team": "离开团队",
|
||||
"user_team_leave_team_failed": "离开团队失败",
|
||||
"waiting": "待接受"
|
||||
"leaved": "已离职",
|
||||
"waiting": "待接受",
|
||||
"sync_immediately": "立即同步",
|
||||
"sync_member_failed": "同步成员失败",
|
||||
"sync_member_success": "同步成员成功"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user