feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -0,0 +1,89 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { size } from 'lodash-es';
import classNames from 'classnames';
import {
IconCozCheckMarkCircleFill,
IconCozInfoCircleFill,
} from '@coze-arch/coze-design/icons';
import { Typography } from '@coze-arch/bot-semi';
import { type TransferResourceInfo } from '@coze-arch/bot-api/playground_api';
interface IResource extends TransferResourceInfo {
spaceID: string;
}
interface IItemGridView {
title: string;
resources: Array<IResource>;
onResourceClick?: (id: string, spaceID: string) => void;
showStatus?: boolean;
}
export function ItemGridView(props: IItemGridView) {
const { title, resources, showStatus = false, onResourceClick } = props;
// HACK: 由于 grid 布局下边界线是透出的背景色,所以 resource 数量为单数的时候需要补齐一个
const isEven = size(resources) % 2 === 0;
const finalResources = isEven
? resources
: [...resources, { name: '', id: '', icon: '', spaceID: '' }];
return (
<>
<p className="text-[12px] leading-[16px] font-[500] coz-fg-secondary text-left align-top w-full mb-[6px]">
{title}
</p>
<div className="mb-[12px]">
<div className="grid grid-cols-2 rounded-[6px] overflow-hidden border border-solid coz-stroke-primary gap-[1px] bg-[var(--coz-stroke-primary)] rounded-[4px]">
{finalResources.map(item => (
<div
key={item.id}
className={classNames(
'flex justify-center items-center gap-x-[4px] p-[8px] w-full coz-bg-plus',
item.id ? 'hover:cursor-pointer' : '',
)}
onClick={() => {
if (item.id) {
onResourceClick?.(item.id, item.spaceID);
}
}}
>
<img
src={item.icon}
className="w-[16px] h-[16px] rounded-[2px]"
/>
<Typography.Text
ellipsis={{ showTooltip: true }}
className="text-[12px] leading-[16px] font-[500] coz-fg-primary text-left align-top grow"
>
{item.name}
</Typography.Text>
{showStatus && item.status === 1 ? (
<div className="coz-fg-hglt-green flex justify-center items-center">
<IconCozCheckMarkCircleFill />
</div>
) : null}
{showStatus && item.status === 0 ? (
<div className="coz-fg-hglt-red flex justify-center items-center">
<IconCozInfoCircleFill />
</div>
) : null}
</div>
))}
</div>
</div>
</>
);
}

View File

@@ -0,0 +1,147 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { size } from 'lodash-es';
import { useRequest, useUnmount } from 'ahooks';
import { I18n } from '@coze-arch/i18n';
import { MoveAction } from '@coze-arch/bot-api/playground_api';
import { type BotSpace } from '@coze-arch/bot-api/developer_api';
import { PlaygroundApi } from '@coze-arch/bot-api';
import { SelectorItem } from '../selector-item';
import { ItemGridView } from '../item-grid-view';
interface IMoveDetailPaneProps {
targetSpace: BotSpace | null;
botID: string;
fromSpaceID: string;
onUnmount?: () => void;
onDetailLoaded?: () => void;
}
export function MoveDetailPane(props: IMoveDetailPaneProps) {
const { targetSpace, botID, fromSpaceID, onUnmount, onDetailLoaded } = props;
const { data: moveDetails } = useRequest(
async () => {
const data = await PlaygroundApi.MoveDraftBot({
bot_id: botID,
target_spaceId: targetSpace.id,
from_spaceId: fromSpaceID,
move_action: MoveAction.Preview,
});
return {
...data?.async_task,
cannotMove: data?.forbid_move,
};
},
{
onSuccess: data => {
if (data && !data.cannotMove) {
onDetailLoaded?.();
}
},
},
);
useUnmount(() => {
onUnmount?.();
});
return (
<div className="flex flex-col">
<div className="w-full border-[0.5px] border-solid coz-stroke-primary mb-[12px]"></div>
<div className="flex flex-col max-h-[406px] overflow-y-auto">
<div className="text-[12px] leading-[16px] font-[500] coz-fg-primary text-left align-top w-full mb-[6px]">
{I18n.t('resource_move_target_team')}
</div>
<div>
<div className="flex flex-col rounded-[6px] overflow-hidden mb-[16px]">
<SelectorItem space={targetSpace} selected disabled />
</div>
</div>
{moveDetails?.cannotMove ? (
<div className="flex items-center gap-x-[8px] p-[12px] w-full coz-mg-hglt-red rounded-[4px] mb-[12px]">
<p className="text-[12px] leading-[16px] font-[400] coz-fg-hglt-red text-left align-top grow">
{I18n.t('move_not_allowed_contain_bot_nodes')}
</p>
</div>
) : null}
{!moveDetails?.cannotMove &&
(size(moveDetails?.transfer_resource_plugin_list) ||
size(moveDetails?.transfer_resource_workflow_list) ||
size(moveDetails?.transfer_resource_knowledge_list)) ? (
<>
<div className="text-[12px] leading-[16px] font-[500] coz-fg-primary text-left align-top w-full mb-[6px]">
{I18n.t('resource_move_together')}
</div>
<div className="flex items-center gap-x-[8px] p-[8px] w-full coz-mg-hglt-red rounded-[4px] mb-[12px]">
<p className="text-[12px] leading-[16px] font-[400] coz-fg-hglt-red text-left align-top grow">
{I18n.t('resource_move_together_desc')}
</p>
</div>
{size(moveDetails?.transfer_resource_plugin_list) > 0 ? (
<ItemGridView
title={I18n.t('store_search_recommend_result2')}
resources={moveDetails.transfer_resource_plugin_list.map(
item => ({
...item,
spaceID: fromSpaceID,
}),
)}
onResourceClick={(id, spaceID) => {
window.open(`/space/${spaceID}/plugin/${id}`);
}}
/>
) : null}
{size(moveDetails?.transfer_resource_workflow_list) > 0 ? (
<ItemGridView
title={I18n.t('store_search_recommend_result3')}
resources={moveDetails.transfer_resource_workflow_list.map(
item => ({
...item,
spaceID: fromSpaceID,
}),
)}
onResourceClick={(id, spaceID) => {
window.open(
`/work_flow?space_id=${spaceID}&workflow_id=${id}`,
);
}}
/>
) : null}
{size(moveDetails?.transfer_resource_knowledge_list) > 0 ? (
<ItemGridView
title={I18n.t('performance_knowledge')}
resources={moveDetails.transfer_resource_knowledge_list.map(
item => ({
...item,
spaceID: fromSpaceID,
}),
)}
onResourceClick={(id, spaceID) => {
window.open(`/space/${spaceID}/knowledge/${id}`);
}}
/>
) : null}
</>
) : null}
</div>
</div>
);
}

View File

@@ -0,0 +1,85 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useState } from 'react';
import { size } from 'lodash-es';
import { I18n } from '@coze-arch/i18n';
import { useSpaceList } from '@coze-arch/bot-studio-store';
import { type BotSpace, SpaceType } from '@coze-arch/bot-api/developer_api';
import { SelectorItem } from '../selector-item';
export function useSelectSpacePane() {
const { spaces } = useSpaceList();
const [targetSpace, setTargetSpace] = useState<BotSpace | null>(null);
const personalSpace = spaces.find(
item => item.space_type === SpaceType.Personal,
);
const teamSpaces = spaces.filter(item => item.space_type === SpaceType.Team);
const selectSpacePane = (
<div className="flex flex-col">
<div className="w-full border-[0.5px] border-solid coz-stroke-primary mb-[12px]"></div>
<div className="flex flex-col max-h-[406px] overflow-y-auto">
<div className="text-[12px] leading-[16px] font-[500] coz-fg-primary text-left align-top w-full mb-[6px]">
{I18n.t('menu_title_personal_space')}
</div>
<div>
<div className="flex flex-col rounded-[6px] overflow-hidden mb-[16px]">
<SelectorItem space={personalSpace} disabled />
</div>
</div>
<div className="text-[12px] leading-[16px] font-[500] coz-fg-primary text-left align-top w-full mb-[6px]">
{I18n.t('resource_move_target_team')}
</div>
<div>
<div className="flex flex-col rounded-[6px] overflow-hidden">
{size(teamSpaces) > 0 ? (
spaces
.filter(item => item.space_type !== SpaceType.Personal)
.map(item => (
<SelectorItem
key={item.id}
space={item}
selected={item.id === targetSpace?.id}
onSelect={space => {
setTargetSpace(space);
}}
/>
))
) : (
<SelectorItem
space={{
// MOCK: 用于展示未加入任何空间的兜底情况
name: I18n.t('resource_move_no_team_joined'),
}}
disabled
/>
)}
</div>
</div>
</div>
</div>
);
return {
targetSpace,
setTargetSpace,
selectSpacePane,
};
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import classnames from 'classnames';
import { IconCozCheckMarkFill } from '@coze-arch/coze-design/icons';
import { type BotSpace } from '@coze-arch/bot-api/developer_api';
interface ISelectorItemProps {
space: BotSpace;
disabled?: boolean;
selected?: boolean;
onSelect?: (space: BotSpace) => void;
}
export function SelectorItem(props: ISelectorItemProps) {
const { space, disabled = false, selected = false, onSelect } = props;
return (
<div
className={classnames(
'flex justify-between items-center gap-x-[8px] p-[8px] w-full coz-mg-primary',
disabled ? '' : 'hover:coz-mg-primary-hovered cursor-pointer',
)}
onClick={() => {
if (!disabled) {
onSelect?.(space);
}
}}
>
<div className="flex items-center">
{space.icon_url ? (
<img
src={space.icon_url}
className="w-[24px] h-[24px] rounded-full mr-[8px]"
/>
) : null}
<p
className={classnames(
'text-[14px] leading-[20px] font-[400] text-left align-middle whitespace-normal -webkit-box line-clamp-1 overflow-hidden grow',
disabled ? 'coz-fg-secondary' : 'coz-fg-primary',
)}
>
{space.name}
</p>
</div>
{selected ? (
<div className="w-[24px] h-[24px] flex justify-center items-center">
<IconCozCheckMarkFill className="coz-fg-secondary" />
</div>
) : null}
</div>
);
}

View File

@@ -0,0 +1,18 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { useBotMoveModal } from './move-modal';
export { useBotMoveFailedModal } from './move-failed-modal';

View File

@@ -0,0 +1,361 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @coze-arch/max-line-per-function -- 不好拆 */
import React, { useCallback, useState } from 'react';
import { size } from 'lodash-es';
import classNames from 'classnames';
import { useBoolean, useRequest } from 'ahooks';
import { withSlardarIdButton } from '@coze-studio/bot-utils';
import { I18n } from '@coze-arch/i18n';
import { IconCozCross } from '@coze-arch/coze-design/icons';
import { Button, Modal, Toast } from '@coze-arch/coze-design';
import { useSpaceList } from '@coze-arch/bot-studio-store';
import { MoveAction } from '@coze-arch/bot-api/playground_api';
import {
DraftBotStatus,
type DraftBot,
SpaceType,
} from '@coze-arch/bot-api/developer_api';
import { PlaygroundApi } from '@coze-arch/bot-api';
import { ItemGridView } from './components/item-grid-view';
interface BotMoveFailedModalOptions {
/**
* botInfo
*/
botInfo: Pick<DraftBot, 'id' | 'name'> | null;
/**
* 更新 bot 状态
*/
onUpdateBotStatus?: (status: DraftBotStatus) => void;
/**
* 迁移成功等效于删除 bot
*/
onMoveSuccess?: () => void;
}
interface UseBotMoveFailedModalValue {
/**
* 打开弹窗的方法
* @param {BotMoveModalOptions} [options] - 此次打开Modal的配置项
*/
open: (options?: BotMoveFailedModalOptions) => void;
/**
* 关闭弹窗的方法
*/
close: () => void;
/**
* 弹窗组件实例,需要手动挂载一下
*/
modalNode: React.ReactNode;
}
const DefaultOptions: BotMoveFailedModalOptions = { botInfo: null };
// eslint-disable-next-line complexity
export function useBotMoveFailedModal(): UseBotMoveFailedModalValue {
const [options, setOptions] =
useState<BotMoveFailedModalOptions>(DefaultOptions);
const [visible, { setTrue: setVisibleTrue, setFalse: setVisibleFalse }] =
useBoolean(false);
const [paneType, setPaneType] = useState<
'detail' | 'confirm_cancel' | 'confirm_force'
>('detail');
const title = (
<span className="mb-[20px] coz-fg-plus text-[16px] font-medium leading-[22px]">
{paneType === 'detail'
? I18n.t('move_failed')
: paneType === 'confirm_cancel'
? I18n.t('move_failed_cancel_confirm_title')
: paneType === 'confirm_force'
? I18n.t('move_failed_force_confirm_title')
: ''}
</span>
);
const open = useCallback((opts?: BotMoveFailedModalOptions) => {
setOptions(opts || DefaultOptions);
setPaneType('detail');
setVisibleTrue();
}, []);
const close = useCallback(() => {
setVisibleFalse();
}, []);
const { spaces } = useSpaceList();
const fromSpaceID =
spaces?.find(s => s.space_type === SpaceType.Personal)?.id ?? '';
const { data: moveDetails } = useRequest(
async () => {
if (!options.botInfo) {
return;
}
const data = await PlaygroundApi.MoveDraftBot({
bot_id: options.botInfo.id,
from_spaceId: fromSpaceID,
move_action: MoveAction.ViewTask,
});
return data.async_task;
},
{ refreshDeps: [options.botInfo] },
);
const { loading, run } = useRequest(
async (moveAction: MoveAction) => {
const data = await PlaygroundApi.MoveDraftBot({
bot_id: options.botInfo.id,
from_spaceId: fromSpaceID,
move_action: moveAction,
});
return { ...data, moveAction };
},
{
manual: true,
onSuccess: data => {
if (data.bot_status === DraftBotStatus.Using) {
if (data.moveAction === MoveAction.CancelTask) {
options.onUpdateBotStatus?.(data.bot_status);
} else {
Toast.success(I18n.t('resource_move_bot_success_toast'));
options.onMoveSuccess?.();
}
close();
} else if (data.bot_status === DraftBotStatus.MoveFail) {
options.onUpdateBotStatus?.(data.bot_status);
Toast.error({
content: withSlardarIdButton(I18n.t('move_failed_toast')),
});
close();
}
},
onError: error => {
Toast.error({
content: withSlardarIdButton(
error?.message || I18n.t('move_failed_toast'),
),
showClose: false,
});
close();
},
},
);
const retry = async () => {
await run(MoveAction.RetryMove);
};
const forceMove = async () => {
await run(MoveAction.ForcedMove);
};
const cancelMove = async () => {
await run(MoveAction.CancelTask);
};
const footer = (
<div
className={classNames(
'coz-modal-footer flex gap-2 justify-end',
'w-full',
)}
>
{paneType === 'detail' ? (
<>
<Button
className="flex-1 !ml-0"
size="large"
color="primary"
disabled={!moveDetails || loading}
onClick={() => {
setPaneType('confirm_cancel');
}}
>
{I18n.t('move_failed_btn_cancel')}
</Button>
<Button
className="flex-1 !ml-0"
size="large"
color="primary"
disabled={!moveDetails || loading}
onClick={() => {
setPaneType('confirm_force');
}}
>
{I18n.t('move_failed_btn_force')}
</Button>
<Button
className="flex-1 !ml-0"
color="brand"
size="large"
loading={loading}
disabled={!moveDetails || loading}
onClick={() => {
retry();
}}
>
{I18n.t('Retry')}
</Button>
</>
) : null}
{paneType === 'confirm_cancel' ? (
<>
<Button
className="!ml-0"
color="primary"
onClick={() => {
setPaneType('detail');
}}
>
{I18n.t('back')}
</Button>
<Button
className="!ml-0"
color="brand"
loading={loading}
onClick={() => {
cancelMove();
}}
>
{I18n.t('confirm')}
</Button>
</>
) : null}
{paneType === 'confirm_force' ? (
<>
<Button
className="!ml-0"
color="primary"
onClick={() => {
setPaneType('detail');
}}
>
{I18n.t('back')}
</Button>
<Button
className="!ml-0"
color="brand"
loading={loading}
onClick={() => {
forceMove();
}}
>
{I18n.t('confirm')}
</Button>
</>
) : null}
</div>
);
const modalNode = (
<Modal
visible={visible}
title={title}
footer={footer}
width={paneType !== 'detail' ? '448px' : '480px'}
footerFill
onCancel={close}
closable={!['confirm_cancel', 'confirm_force'].includes(paneType)}
maskClosable={false}
keepDOM={false}
closeIcon={<IconCozCross className="coz-fg-secondary" />}
>
{paneType === 'detail' ? (
<div className="flex flex-col">
<div className="w-full border-[0.5px] border-solid coz-stroke-primary mb-[12px]"></div>
<div className="flex flex-col max-h-[406px] overflow-y-auto">
<div className="flex items-center gap-x-[8px] p-[8px] w-full coz-mg-primary rounded-[6px] mb-[12px]">
<p className="text-[12px] leading-[16px] font-[400] coz-fg-secondary text-left align-top grow">
{I18n.t('move_failed_desc')}
</p>
</div>
{size(moveDetails?.transfer_resource_plugin_list) > 0 ? (
<ItemGridView
title={I18n.t('store_search_recommend_result2')}
resources={moveDetails?.transfer_resource_plugin_list.map(
item => ({
...item,
spaceID: item.status
? moveDetails?.task_info.TargetSpaceId
: moveDetails?.task_info.OriSpaceId,
}),
)}
onResourceClick={(id, spaceID) => {
window.open(`/space/${spaceID}/plugin/${id}`);
}}
showStatus
/>
) : null}
{size(moveDetails?.transfer_resource_workflow_list) > 0 ? (
<ItemGridView
title={I18n.t('store_search_recommend_result3')}
resources={moveDetails?.transfer_resource_workflow_list.map(
item => ({
...item,
spaceID: item.status
? moveDetails?.task_info.TargetSpaceId
: moveDetails?.task_info.OriSpaceId,
}),
)}
onResourceClick={(id, spaceID) => {
window.open(
`/work_flow?space_id=${spaceID}&workflow_id=${id}`,
);
}}
showStatus
/>
) : null}
{size(moveDetails?.transfer_resource_knowledge_list) > 0 ? (
<ItemGridView
title={I18n.t('performance_knowledge')}
resources={moveDetails?.transfer_resource_knowledge_list.map(
item => ({
...item,
spaceID: item.status
? moveDetails?.task_info.TargetSpaceId
: moveDetails?.task_info.OriSpaceId,
}),
)}
onResourceClick={(id, spaceID) => {
window.open(`/space/${spaceID}/knowledge/${id}`);
}}
showStatus
/>
) : null}
</div>
</div>
) : null}
{paneType === 'confirm_force' ? (
<div className="mt-[20px]">
{I18n.t('move_failed_force_confirm_content')}
</div>
) : null}
{paneType === 'confirm_cancel' ? (
<div className="mt-[20px]">
{I18n.t('move_failed_cancel_confirm_content')}
</div>
) : null}
</Modal>
);
return {
modalNode,
open,
close,
};
}

View File

@@ -0,0 +1,293 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @coze-arch/max-line-per-function -- 难拆*/
import React, { useCallback, useState } from 'react';
import classNames from 'classnames';
import { useBoolean, useRequest } from 'ahooks';
import { I18n } from '@coze-arch/i18n';
import { useSpaceList } from '@coze-arch/bot-studio-store';
import { MoveAction } from '@coze-arch/bot-api/playground_api';
import {
DraftBotStatus,
type DraftBot,
SpaceType,
} from '@coze-arch/bot-api/developer_api';
import { PlaygroundApi } from '@coze-arch/bot-api';
import { withSlardarIdButton } from '@coze-studio/bot-utils';
import { cozeMitt } from '@coze-common/coze-mitt';
import { IconInfo } from '@coze-arch/bot-icons';
import { IconCozCross } from '@coze-arch/coze-design/icons';
import {
Button,
IconButton,
Modal,
Toast,
Tooltip,
Typography,
} from '@coze-arch/coze-design';
import { useSelectSpacePane } from './components/select-space-pane';
import { MoveDetailPane } from './components/move-detail-pane';
interface BotMoveModalOptions {
/**
* botInfo
*/
botInfo: Pick<DraftBot, 'name' | 'id'> | null;
/**
* 更新 bot 状态
*/
onUpdateBotStatus?: (status: DraftBotStatus) => void;
/**
* 迁移成功等效于删除 bot
*/
onMoveSuccess?: () => void;
/**
* 关闭 modal
*/
onClose?: () => void;
}
interface UseBotMoveModalValue {
/**
* 打开弹窗的方法
* @param {BotMoveModalOptions} [options] - 此次打开Modal的配置项
*/
open: (options?: BotMoveModalOptions) => void;
/**
* 关闭弹窗的方法
*/
close: () => void;
/**
* 弹窗组件实例,需要手动挂载一下
*/
modalNode: React.ReactNode;
}
const DefaultOptions: BotMoveModalOptions = { botInfo: null };
export function useBotMoveModal(): UseBotMoveModalValue {
const [options, setOptions] = useState<BotMoveModalOptions>(DefaultOptions);
const [visible, { setTrue: setVisibleTrue, setFalse: setVisibleFalse }] =
useBoolean(false);
const [paneType, setPaneType] = useState<'select' | 'move' | 'confirm'>(
'select',
);
const { targetSpace, selectSpacePane, setTargetSpace } = useSelectSpacePane();
const title =
paneType !== 'confirm' ? (
<div className="flex justify-start items-center mb-[24px] w-[380px]">
<div className="coz-fg-plus text-[16px] font-medium leading-[22px] max-w-full">
<Typography.Text
ellipsis={{
showTooltip: true,
}}
className="text-[16px]"
>
{I18n.t('resource_move_title', {
bot_name: options.botInfo?.name ?? '',
})}
</Typography.Text>
</div>
<Tooltip content={I18n.t('resource_move_notice')}>
<IconButton
size="small"
color="secondary"
icon={<IconInfo className="coz-fg-secondary" />}
/>
</Tooltip>
</div>
) : (
I18n.t('resource_move_confirm_title')
);
const open = useCallback((opts?: BotMoveModalOptions) => {
setOptions(opts || DefaultOptions);
setPaneType('select');
setTargetSpace(null);
setVisibleTrue();
}, []);
const close = useCallback(() => {
setVisibleFalse();
}, []);
const { spaces } = useSpaceList();
const fromSpaceID = spaces.find(s => s.space_type === SpaceType.Personal)?.id;
const { loading: moveLoading, run: moveBot } = useRequest(
async () => {
const data = await PlaygroundApi.MoveDraftBot({
bot_id: options.botInfo.id,
target_spaceId: targetSpace.id,
from_spaceId: fromSpaceID,
move_action: MoveAction.Move,
});
return data;
},
{
manual: true,
onSuccess: data => {
if (data.bot_status === DraftBotStatus.Using) {
Toast.success(I18n.t('resource_move_bot_success_toast'));
options.onMoveSuccess?.();
close();
cozeMitt.emit('refreshFavList', {
numDelta: -1,
});
} else if (data.bot_status === DraftBotStatus.MoveFail) {
options.onUpdateBotStatus?.(data.bot_status);
Toast.error({
content: withSlardarIdButton(I18n.t('move_failed_toast')),
});
close();
}
},
onError: error => {
Toast.error({
content: withSlardarIdButton(
error?.message || I18n.t('move_failed_toast'),
),
showClose: false,
});
close();
},
},
);
const onConfirm = async () => {
await moveBot();
};
const [moveDisabled, setMoveDisabled] = useState(true);
const footer = (
<div
className={classNames(
'coz-modal-footer flex gap-2 justify-end',
paneType !== 'confirm' && 'w-full',
)}
>
{paneType === 'select' ? (
<Button
className="flex-1 !ml-0"
color="brand"
size="large"
disabled={!targetSpace}
onClick={() => {
setPaneType('move');
}}
>
{I18n.t('next')}
</Button>
) : null}
{paneType === 'move' ? (
<>
<Button
className="flex-1 !ml-0"
size="large"
color="primary"
onClick={() => {
setPaneType('select');
}}
>
{I18n.t('back')}
</Button>
<Button
className="flex-1 !ml-0"
color="brand"
size="large"
disabled={moveDisabled}
onClick={() => {
setPaneType('confirm');
}}
>
{I18n.t('resource_move')}
</Button>
</>
) : null}
{paneType === 'confirm' ? (
<>
<Button
className="!ml-0"
color="primary"
onClick={() => {
setPaneType('move');
}}
>
{I18n.t('back')}
</Button>
<Button
className="!ml-0"
color="brand"
loading={moveLoading}
onClick={() => {
onConfirm();
}}
>
{I18n.t('confirm')}
</Button>
</>
) : null}
</div>
);
const modalNode = (
<Modal
visible={visible}
title={title}
footer={footer}
width={paneType === 'confirm' ? '448px' : '480px'}
footerFill
onCancel={() => {
close?.();
options.onClose?.();
}}
closable={paneType !== 'confirm'}
maskClosable={false}
keepDOM={false}
closeIcon={<IconCozCross className="coz-fg-secondary" />}
>
{paneType === 'select' ? selectSpacePane : null}
{paneType === 'move' ? (
<>
<MoveDetailPane
targetSpace={targetSpace}
botID={options.botInfo.id}
fromSpaceID={fromSpaceID}
onUnmount={() => setMoveDisabled(true)}
onDetailLoaded={() => setMoveDisabled(false)}
/>
{IS_CN_REGION ? (
<div className="coz-fg-hglt-red">{I18n.t('move_desc1')}</div>
) : null}
</>
) : null}
{paneType === 'confirm' ? (
<div className="mt-[20px]">
{I18n.t('resource_move_confirm_content')}
</div>
) : null}
</Modal>
);
return {
modalNode,
open,
close,
};
}