feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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 { I18n } from '@coze-arch/i18n';
|
||||
|
||||
export const REAL_DATA_ID = '0';
|
||||
|
||||
export const REAL_DATA_MOCKSET = {
|
||||
id: REAL_DATA_ID,
|
||||
name: I18n.t('real_data'),
|
||||
};
|
||||
|
||||
// 初始化仅有real_data
|
||||
export const MOCK_OPTION_LIST = [REAL_DATA_MOCKSET];
|
||||
|
||||
export const POLLING_INTERVAL = 10000;
|
||||
|
||||
export const DELAY_TIME = 2000;
|
||||
|
||||
export const CONNECTOR_ID = '10000010';
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* 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 { create } from 'zustand';
|
||||
import { produce } from 'immer';
|
||||
import {
|
||||
type BizCtx,
|
||||
type MockSet,
|
||||
type MockSetBinding,
|
||||
} from '@coze-arch/bot-api/debugger_api';
|
||||
import { type BasicMockSetInfo } from '@coze-studio/mockset-shared';
|
||||
|
||||
import { isCurrent } from '../../util';
|
||||
|
||||
export interface EnabledMockSetInfo {
|
||||
mockSetBinding: MockSetBinding;
|
||||
mockSetDetail?: MockSet;
|
||||
}
|
||||
|
||||
interface MockInfoStoreState {
|
||||
bizCtx: BizCtx;
|
||||
enabledMockSetInfo: Array<EnabledMockSetInfo>;
|
||||
isPolling: boolean;
|
||||
isLoading: boolean;
|
||||
currentMockComp: Array<BasicMockSetInfo>;
|
||||
timer?: NodeJS.Timeout;
|
||||
restartTimer?: NodeJS.Timeout;
|
||||
}
|
||||
|
||||
interface MockInfoStoreAction {
|
||||
setPolling: (polling: boolean) => void;
|
||||
setLoading: (loading: boolean) => void;
|
||||
setCurrentBizCtx: (bizCtx: BizCtx) => void;
|
||||
setEnabledMockSetInfo: (mockSetList?: Array<EnabledMockSetInfo>) => void;
|
||||
removeMockComp: (mockComp: BasicMockSetInfo) => number;
|
||||
addMockComp: (mockComp: BasicMockSetInfo) => number;
|
||||
setTimer: (timer?: NodeJS.Timeout) => void;
|
||||
setRestartTimer: (timer?: NodeJS.Timeout) => void;
|
||||
}
|
||||
|
||||
export const useMockInfoStore = create<
|
||||
MockInfoStoreState & MockInfoStoreAction
|
||||
>((set, get) => ({
|
||||
bizCtx: {},
|
||||
enabledMockSetInfo: [],
|
||||
isPolling: false,
|
||||
isLoading: false,
|
||||
currentMockComp: [],
|
||||
setPolling: polling => {
|
||||
set({ isPolling: polling });
|
||||
},
|
||||
setLoading: loading => {
|
||||
set({ isLoading: loading });
|
||||
},
|
||||
setCurrentBizCtx: bizCtx => {
|
||||
set({ bizCtx });
|
||||
},
|
||||
setEnabledMockSetInfo: enabledMockSetInfo => {
|
||||
set({ enabledMockSetInfo });
|
||||
},
|
||||
addMockComp: mockSetInfo => {
|
||||
set(
|
||||
produce<MockInfoStoreState>(s => {
|
||||
const index = s.currentMockComp.findIndex(item =>
|
||||
isCurrent(item, mockSetInfo),
|
||||
);
|
||||
index <= -1 && s.currentMockComp.push(mockSetInfo);
|
||||
}),
|
||||
);
|
||||
return get().currentMockComp.length;
|
||||
},
|
||||
removeMockComp: mockSetInfo => {
|
||||
set(
|
||||
produce<MockInfoStoreState>(s => {
|
||||
const index = s.currentMockComp.findIndex(item =>
|
||||
isCurrent(item, mockSetInfo),
|
||||
);
|
||||
if (index > -1) {
|
||||
s.currentMockComp.splice(index, 1);
|
||||
}
|
||||
}),
|
||||
);
|
||||
return get().currentMockComp.length;
|
||||
},
|
||||
setTimer: timer => {
|
||||
set({ timer });
|
||||
},
|
||||
setRestartTimer: timer => {
|
||||
set({ timer });
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,196 @@
|
||||
/*
|
||||
* 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 { useEffect, useRef } from 'react';
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
import axios, { type Canceler } from 'axios';
|
||||
import { logger } from '@coze-arch/logger';
|
||||
import {
|
||||
type BizCtx,
|
||||
type MockSet,
|
||||
type MockSetBinding,
|
||||
} from '@coze-arch/bot-api/debugger_api';
|
||||
import { debuggerApi } from '@coze-arch/bot-api';
|
||||
|
||||
import { MockTrafficEnabled } from '../../util/get-mock-set-options';
|
||||
import { isSameScene } from '../../util';
|
||||
import { type EnabledMockSetInfo, useMockInfoStore } from './store';
|
||||
|
||||
function combineBindMockSetInfo(
|
||||
mockSetBindingList: Array<MockSetBinding>,
|
||||
mockSetDetailSet: Record<string, MockSet>,
|
||||
): Array<EnabledMockSetInfo> {
|
||||
return mockSetBindingList.map(mockSetInfo => {
|
||||
const { mockSetID } = mockSetInfo;
|
||||
const detail = mockSetID ? mockSetDetailSet[mockSetID] : {};
|
||||
return {
|
||||
mockSetBinding: mockSetInfo,
|
||||
mockSetDetail: detail,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
export const useInitialGetEnabledMockSet = ({
|
||||
bizCtx,
|
||||
pollingInterval,
|
||||
}: {
|
||||
bizCtx: BizCtx;
|
||||
pollingInterval?: number;
|
||||
}) => {
|
||||
const {
|
||||
enabledMockSetInfo,
|
||||
setPolling,
|
||||
setEnabledMockSetInfo,
|
||||
bizCtx: currentBizCtx,
|
||||
setCurrentBizCtx,
|
||||
addMockComp,
|
||||
removeMockComp,
|
||||
isPolling,
|
||||
setTimer,
|
||||
timer,
|
||||
currentMockComp,
|
||||
setLoading,
|
||||
isLoading,
|
||||
restartTimer,
|
||||
setRestartTimer,
|
||||
} = useMockInfoStore();
|
||||
const status = useRef<boolean>(false);
|
||||
const lastRequestId = useRef(0);
|
||||
const pollingTurnRef = useRef<string>();
|
||||
|
||||
const cancelReq = useRef<Canceler>();
|
||||
|
||||
const requestFn = async (curBizCtx: BizCtx) => {
|
||||
const currentRequestId = ++lastRequestId.current;
|
||||
const currentPollingTurn = pollingTurnRef.current;
|
||||
try {
|
||||
const { mockSetBindings = [], mockSetDetails = {} } =
|
||||
await debuggerApi.MGetMockSetBinding(
|
||||
{
|
||||
bizCtx: curBizCtx,
|
||||
needMockSetDetail: true,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'rpc-persist-mock-traffic-enable': MockTrafficEnabled.ENABLE,
|
||||
},
|
||||
cancelToken: new axios.CancelToken(function executor(c) {
|
||||
cancelReq.current = c;
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
if (
|
||||
(currentRequestId > 1 && currentRequestId !== lastRequestId.current) ||
|
||||
!pollingTurnRef.current ||
|
||||
pollingTurnRef.current !== currentPollingTurn
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setEnabledMockSetInfo?.(
|
||||
combineBindMockSetInfo(mockSetBindings, mockSetDetails),
|
||||
);
|
||||
return { mockSetBindings, mockSetDetails };
|
||||
} catch (e) {
|
||||
if (axios.isCancel(e)) {
|
||||
logger.info('poll_scene_mockset_canceled');
|
||||
} else {
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
logger.error({ error: e, eventName: 'poll_scene_mockset_fail' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const request = async () => {
|
||||
try {
|
||||
const {
|
||||
trafficCallerID,
|
||||
connectorID,
|
||||
connectorUID,
|
||||
bizSpaceID,
|
||||
trafficScene,
|
||||
} = bizCtx;
|
||||
!status.current && (status.current = true);
|
||||
setLoading(true);
|
||||
if (status.current && pollingInterval) {
|
||||
setPolling(true);
|
||||
const id = setTimeout(() => {
|
||||
status.current && request();
|
||||
}, pollingInterval);
|
||||
setTimer(id);
|
||||
}
|
||||
await requestFn({
|
||||
trafficCallerID,
|
||||
connectorID,
|
||||
connectorUID,
|
||||
bizSpaceID,
|
||||
trafficScene,
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 取消
|
||||
const cancel = () => {
|
||||
pollingTurnRef.current = undefined;
|
||||
cancelReq.current?.();
|
||||
lastRequestId.current = 0;
|
||||
cancelRestartTask();
|
||||
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
setPolling(false);
|
||||
setTimer(undefined);
|
||||
status.current && (status.current = false);
|
||||
}
|
||||
};
|
||||
|
||||
const cancelRestartTask = () => {
|
||||
if (restartTimer) {
|
||||
clearTimeout(restartTimer);
|
||||
setRestartTimer(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
const start = async () => {
|
||||
cancel();
|
||||
pollingTurnRef.current = nanoid();
|
||||
await request();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (currentBizCtx && isSameScene(bizCtx, currentBizCtx)) {
|
||||
return;
|
||||
}
|
||||
setCurrentBizCtx(bizCtx);
|
||||
}, [bizCtx]);
|
||||
|
||||
return {
|
||||
start,
|
||||
cancel,
|
||||
isLoading,
|
||||
data: enabledMockSetInfo,
|
||||
addMockComp,
|
||||
removeMockComp,
|
||||
currentMockComp,
|
||||
isPolling,
|
||||
setRestartTimer,
|
||||
restartTimer,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,157 @@
|
||||
.layout-header {
|
||||
padding: 16px 24px 24px;
|
||||
}
|
||||
|
||||
|
||||
.content-title {
|
||||
max-width: 1160px;
|
||||
margin: 0 auto 16px;
|
||||
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 32px;
|
||||
color: var(--semi-color-text-0);
|
||||
}
|
||||
|
||||
.list-container_scroll {
|
||||
width: 100%;
|
||||
max-width: 1160px;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.list-container_flexible,
|
||||
.list-container-no-header_flexible {
|
||||
width: 100%;
|
||||
max-width: 1160px;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.list-container_flexible {
|
||||
height: calc(100% - 72px);
|
||||
}
|
||||
|
||||
.list-container-no-header_flexible {
|
||||
height: calc(100%);
|
||||
}
|
||||
|
||||
/** mock-set-intro */
|
||||
|
||||
.mock-set-intro-title {
|
||||
width: 100%;
|
||||
margin-top: 2px;
|
||||
margin-bottom: 4px;
|
||||
|
||||
&.mock-set-intro-title_full {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.mock-set-intro-name {
|
||||
overflow: hidden;
|
||||
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 32px;
|
||||
color: var(--semi-color-text-0);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mock-set-intro-name_full {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.mock-set-intro-edit {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--semi-color-text-2)
|
||||
}
|
||||
|
||||
.mock-set-intro-edit_full,
|
||||
.mock-set-intro-edit_full svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.mock-set-intro-desc_priority.mock-set-intro-desc {
|
||||
overflow: hidden;
|
||||
|
||||
width: 100%;
|
||||
|
||||
font-size: 12px;
|
||||
line-height: 22px;
|
||||
color: var(--semi-color-text-1);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mock-set-intro-desc_priority.mock-set-intro-desc_full {
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
/** 创建框 **/
|
||||
.mock-creation-modal {
|
||||
:global {
|
||||
.semi-modal-content .semi-modal-body {
|
||||
/** 保证内部 tooltip 不被遮盖 **/
|
||||
overflow: unset
|
||||
}
|
||||
|
||||
.semi-modal-footer .semi-button{
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.mock-creation-card {
|
||||
height: calc(100% - 24px);
|
||||
}
|
||||
|
||||
div.mock-creation-modal-editor {
|
||||
/** 兼容 modal 在小窗口下 body 高度不生效的问题 */
|
||||
height: calc(100vh - 316px);
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
.mock-creation-card-editor {
|
||||
height: calc(100% - 40px - 32px - 48px);
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.mock-creation-card-operation {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/** mock-data-list **/
|
||||
.skeleton {
|
||||
:global {
|
||||
.semi-skeleton-image {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty {
|
||||
justify-content: center;
|
||||
height: calc(100% - 68px);
|
||||
padding-bottom: 10%;
|
||||
|
||||
:global {
|
||||
.semi-empty-image svg {
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
}
|
||||
|
||||
.semi-empty-description {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
.long-text.long-text-tooltip {
|
||||
color: var(--semi-color-bg-0);
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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 { type Ellipsis, type TextProps } from '@coze-arch/bot-semi/Typography';
|
||||
import { Typography } from '@coze-arch/bot-semi';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
interface LongTextWithTooltip extends TextProps {
|
||||
tooltipText?: string;
|
||||
}
|
||||
|
||||
export function LongTextWithTooltip(props: LongTextWithTooltip) {
|
||||
const { children, ellipsis, tooltipText, ...rest } = props;
|
||||
|
||||
const ellipsisConfig: boolean | Ellipsis | undefined =
|
||||
ellipsis === false
|
||||
? ellipsis
|
||||
: {
|
||||
showTooltip: {
|
||||
opts: {
|
||||
content: (
|
||||
<Typography.Text
|
||||
className={classNames(s['long-text-tooltip'], s['long-text'])}
|
||||
onClick={e => e.stopPropagation()}
|
||||
ellipsis={{ showTooltip: false, rows: 16 }}
|
||||
>
|
||||
{tooltipText || props.children}
|
||||
</Typography.Text>
|
||||
),
|
||||
},
|
||||
},
|
||||
...(typeof ellipsis !== 'object' ? {} : ellipsis),
|
||||
};
|
||||
|
||||
return (
|
||||
<Typography.Text ellipsis={ellipsisConfig} {...rest}>
|
||||
{props.children}
|
||||
</Typography.Text>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
.mock-data-content {
|
||||
overflow: auto;
|
||||
|
||||
min-height: 64px;
|
||||
max-height: 500px;
|
||||
|
||||
word-wrap: break-word;
|
||||
|
||||
border: 1px var(--semi-color-border) solid;
|
||||
border-radius: 8px;
|
||||
|
||||
:global {
|
||||
.semi-tree-option-list-block .semi-tree-option-selected {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.semi-tree-option-list-block .semi-tree-option:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mock-data-card-operations {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
|
||||
padding: 4px;
|
||||
|
||||
visibility: hidden;
|
||||
background: #F7F7FA;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px rgb(0 0 0 / 20%);
|
||||
}
|
||||
|
||||
.mock-data-card {
|
||||
position: relative;
|
||||
margin-bottom: 16px;
|
||||
|
||||
&:hover .mock-data-card-operations {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.mock-data-card-edit,
|
||||
.mock-data-card-delete {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.mock-data-content-code {
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.mock-data-banner {
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
left: 20%;
|
||||
width: 60%;
|
||||
|
||||
:global {
|
||||
.semi-banner-icon {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.card-item {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.card-item_deleted {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.card-item-text {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
color: #1D1C23;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.card-item-text_required,
|
||||
.card-item-text_highlighted {
|
||||
color: var(--semi-color-danger);
|
||||
}
|
||||
|
||||
.card-item-text_primary {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.card-item-text_invalid {
|
||||
color: var(--semi-color-text-3);
|
||||
}
|
||||
|
||||
.card-item-text_stretched {
|
||||
overflow: hidden;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.card-item-text_wrap {
|
||||
white-space: normal;
|
||||
|
||||
}
|
||||
|
||||
.card-item-tag {
|
||||
padding: 2px 8px;
|
||||
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
color: #6B6B75;
|
||||
word-wrap: break-word;
|
||||
|
||||
background: rgb(46 46 56 / 8%);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.card-branches {
|
||||
pointer-events: none;
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 8px;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.card-branch-v {
|
||||
display: inline-block;
|
||||
|
||||
width: 13px;
|
||||
height: 100%;
|
||||
margin-left: 6px;
|
||||
|
||||
vertical-align: top;
|
||||
|
||||
border-left: 1px solid transparent;
|
||||
}
|
||||
|
||||
.card-branch-v_visible {
|
||||
border-color: #C6C6CD;
|
||||
}
|
||||
|
||||
.card-branch-v_half {
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.card-branch-h {
|
||||
display: inline-block;
|
||||
|
||||
width: 6px;
|
||||
height: 15px;
|
||||
margin-left: -13px;
|
||||
|
||||
vertical-align: top;
|
||||
|
||||
border-color: #C6C6CD;
|
||||
border-style: solid;
|
||||
border-width: 0 0 1px 1px;
|
||||
border-radius: 0 0 0 4px;
|
||||
}
|
||||
|
||||
.card-branch-h_long {
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.card-non-tree-container {
|
||||
padding: 8px 30px;
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
/*
|
||||
* 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 classNames from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { type TreeNodeData } from '@coze-arch/bot-semi/Tree';
|
||||
import { Space, Tree, UIIconButton } from '@coze-arch/bot-semi';
|
||||
import { IconEditNew, IconDeleteOutline } from '@coze-arch/bot-icons';
|
||||
import { type infra, type MockRule } from '@coze-arch/bot-api/debugger_api';
|
||||
import { ROOT_KEY } from '@coze-studio/mockset-shared';
|
||||
|
||||
import { transUpperCase } from '../../util/utils';
|
||||
import {
|
||||
type MockDataInfo,
|
||||
MockDataStatus,
|
||||
MockDataValueType,
|
||||
type MockDataWithStatus,
|
||||
} from '../../util/typings';
|
||||
import { useTransSchema } from '../../hook/use-trans-schema';
|
||||
import { BranchType, useGenTreeBranch } from '../../hook/use-gen-tree-branch';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
interface MockDataCardProps {
|
||||
mock?: MockRule;
|
||||
schema?: string;
|
||||
readOnly?: boolean;
|
||||
className?: string;
|
||||
|
||||
onEdit?: (params: MockDataInfo) => void;
|
||||
onRemove?: (params: MockDataInfo) => void;
|
||||
bizCtx: infra.BizCtx;
|
||||
}
|
||||
|
||||
/** mock data 展示卡片 */
|
||||
export function MockDataCard({
|
||||
mock,
|
||||
readOnly,
|
||||
schema,
|
||||
onEdit,
|
||||
onRemove,
|
||||
}: MockDataCardProps) {
|
||||
const { formattedResultExample, incompatible, mergedResult } = useTransSchema(
|
||||
schema,
|
||||
mock?.responseExpect?.responseExpectRule,
|
||||
);
|
||||
|
||||
const { branchInfo, prunedData } = useGenTreeBranch(mergedResult);
|
||||
|
||||
const deleteHandler = () => {
|
||||
onRemove?.({
|
||||
schema,
|
||||
mock,
|
||||
});
|
||||
};
|
||||
|
||||
const editHandler = () => {
|
||||
onEdit?.({
|
||||
schema,
|
||||
mock,
|
||||
mergedResultExample: formattedResultExample,
|
||||
incompatible,
|
||||
});
|
||||
};
|
||||
|
||||
const renderBranches = (item: MockDataWithStatus, isLevel0Item: boolean) => {
|
||||
const branchThisRow = item?.key ? branchInfo[item.key] : undefined;
|
||||
|
||||
return (
|
||||
<span className={s['card-branches']}>
|
||||
{branchThisRow?.v.map((type, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className={classNames(
|
||||
s['card-branch-v'],
|
||||
type !== BranchType.NONE ? s['card-branch-v_visible'] : '',
|
||||
type === BranchType.HALF ? s['card-branch-v_half'] : '',
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
{!isLevel0Item ? (
|
||||
<span
|
||||
className={classNames(
|
||||
s['card-branch-h'],
|
||||
item?.children ? '' : s['card-branch-h_long'],
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const renderFieldContent = (item: MockDataWithStatus) => {
|
||||
const isRemoved = item?.status === MockDataStatus.REMOVED;
|
||||
|
||||
if (item?.status === MockDataStatus.ADDED) {
|
||||
return (
|
||||
<MockDataValueSpan
|
||||
val={
|
||||
item.isRequired
|
||||
? I18n.t('mockset_field_is_required', { field: item?.label })
|
||||
: undefined
|
||||
}
|
||||
className={classNames(
|
||||
'ms-[8px]',
|
||||
item.isRequired ? s['card-item-text_highlighted'] : '',
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (item?.type === MockDataValueType.ARRAY ||
|
||||
item?.type === MockDataValueType.OBJECT) &&
|
||||
item?.children ? (
|
||||
''
|
||||
) : (
|
||||
<MockDataValueSpan
|
||||
val={item?.displayValue}
|
||||
className={classNames(
|
||||
'ms-[8px]',
|
||||
isRemoved ? s['card-item-text_highlighted'] : '',
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
const renderLabel = (_, node?: TreeNodeData) => {
|
||||
const item = node as MockDataWithStatus | undefined;
|
||||
const isLevel0Item = `${ROOT_KEY}-${item?.label}` === item?.key;
|
||||
const isRemoved = item?.status === MockDataStatus.REMOVED;
|
||||
const isAdded = item?.status === MockDataStatus.ADDED && item.isRequired;
|
||||
|
||||
return item ? (
|
||||
<>
|
||||
{renderBranches(item, isLevel0Item)}
|
||||
|
||||
<span
|
||||
className={classNames(
|
||||
s['card-item'],
|
||||
isRemoved || isAdded ? s['card-item-text_highlighted'] : '',
|
||||
isRemoved ? s['card-item_deleted'] : '',
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={classNames(
|
||||
s['card-item-text'],
|
||||
isLevel0Item ? s['card-item-text_primary'] : '',
|
||||
isRemoved || isAdded ? s['card-item-text_highlighted'] : '',
|
||||
)}
|
||||
>
|
||||
{item?.label}
|
||||
</span>
|
||||
|
||||
{item?.isRequired ? (
|
||||
<span
|
||||
className={classNames(
|
||||
s['card-item-text'],
|
||||
s['card-item-text_required'],
|
||||
)}
|
||||
>
|
||||
*
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
{!isRemoved && !isAdded ? (
|
||||
<span className={classNames(s['card-item-tag'], 'ms-[8px]')}>
|
||||
{transUpperCase(item?.type)}
|
||||
{item?.type === MockDataValueType.ARRAY
|
||||
? `<${transUpperCase(item?.childrenType)}>`
|
||||
: ''}
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
{renderFieldContent(item)}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
);
|
||||
};
|
||||
|
||||
const renderData = () => {
|
||||
if (
|
||||
prunedData?.type === MockDataValueType.ARRAY ||
|
||||
prunedData?.type === MockDataValueType.OBJECT
|
||||
) {
|
||||
if (prunedData.children?.length) {
|
||||
return (
|
||||
<Tree
|
||||
defaultExpandAll
|
||||
treeData={prunedData.children}
|
||||
renderLabel={renderLabel}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className={s['card-non-tree-container']}>
|
||||
<span
|
||||
className={classNames(
|
||||
s['card-item-text'],
|
||||
s['card-item-text_invalid'],
|
||||
)}
|
||||
>
|
||||
Empty
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return (
|
||||
<div className={s['card-non-tree-container']}>
|
||||
<MockDataValueSpan val={prunedData?.displayValue} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return mock?.responseExpect?.responseExpectRule ? (
|
||||
<div className={s['mock-data-card']}>
|
||||
<div className={s['mock-data-content']}>{renderData()}</div>
|
||||
{!readOnly ? (
|
||||
<Space className={s['mock-data-card-operations']} spacing={12}>
|
||||
<UIIconButton
|
||||
icon={<IconEditNew className={s['mock-data-card-edit']} />}
|
||||
size="small"
|
||||
theme="borderless"
|
||||
onClick={editHandler}
|
||||
/>
|
||||
<UIIconButton
|
||||
icon={<IconDeleteOutline className={s['mock-data-card-edit']} />}
|
||||
size="small"
|
||||
theme="borderless"
|
||||
onClick={deleteHandler}
|
||||
/>
|
||||
</Space>
|
||||
) : null}
|
||||
</div>
|
||||
) : null;
|
||||
}
|
||||
|
||||
const MockDataValueSpan = (props: { val?: string; className?: string }) =>
|
||||
props.val ? (
|
||||
<span className={classNames(props.className, s['card-item-text'])}>
|
||||
{props.val}
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
className={classNames(
|
||||
props.className,
|
||||
s['card-item-text'],
|
||||
s['card-item-text_invalid'],
|
||||
)}
|
||||
>
|
||||
Undefined
|
||||
</span>
|
||||
);
|
||||
@@ -0,0 +1,279 @@
|
||||
/*
|
||||
* 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 { useParams } from 'react-router-dom';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { type DynamicParams } from '@coze-arch/bot-typings/teamspace';
|
||||
import {
|
||||
type PluginMockSetCommonParams,
|
||||
type PluginMockDataGenerateMode,
|
||||
} from '@coze-arch/bot-tea';
|
||||
import { useSpaceStore } from '@coze-arch/bot-studio-store';
|
||||
import {
|
||||
Space,
|
||||
Toast,
|
||||
UIButton,
|
||||
UIModal,
|
||||
Typography,
|
||||
Divider,
|
||||
} from '@coze-arch/bot-semi';
|
||||
import { SpaceType } from '@coze-arch/bot-api/playground_api';
|
||||
import { type mockset, type infra } from '@coze-arch/bot-api/debugger_api';
|
||||
import {
|
||||
calcStringSize,
|
||||
type MockDataInfo,
|
||||
MAX_SUBMIT_LENGTH,
|
||||
getEnvironment,
|
||||
} from '@coze-studio/mockset-shared';
|
||||
import {
|
||||
MocksetEditor,
|
||||
type EditorAreaActions,
|
||||
} from '@coze-studio/mockset-editor-adapter';
|
||||
|
||||
import {
|
||||
PRE_DEFINED_NO_EMPTY_KEY,
|
||||
useTransSchema,
|
||||
} from '../hook/use-trans-schema';
|
||||
import { useSaveMockData } from '../hook/use-save-mock-data';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
export enum CreationMode {
|
||||
/** 弹窗形式 */
|
||||
MODAL = 'modal',
|
||||
/** 嵌入页面 */
|
||||
CARD = 'card',
|
||||
}
|
||||
|
||||
interface MockDataCreateCardProps {
|
||||
mode: CreationMode;
|
||||
mockInfo?: MockDataInfo;
|
||||
// mode 为 modal 时生效
|
||||
visible?: boolean;
|
||||
// mode 为 modal 时生效
|
||||
onCancel?: () => void;
|
||||
onSuccess: (data?: mockset.MockRule[]) => void;
|
||||
bizCtx: infra.BizCtx;
|
||||
forceGenerate?: {
|
||||
mode: PluginMockDataGenerateMode;
|
||||
count: number;
|
||||
};
|
||||
}
|
||||
|
||||
/** 创建or编辑 mock data - */
|
||||
export function MockDataCreateCard({
|
||||
mode,
|
||||
mockInfo,
|
||||
visible,
|
||||
onCancel,
|
||||
onSuccess,
|
||||
bizCtx,
|
||||
forceGenerate,
|
||||
}: MockDataCreateCardProps) {
|
||||
const { schema } = mockInfo || {};
|
||||
const editorsRef = useRef<EditorAreaActions>(null);
|
||||
const [disableSubmit, setDisableSubmit] = useState(false);
|
||||
const [disableSubmitWhenGenerating, setDisableSubmitWhenGenerating] =
|
||||
useState(false);
|
||||
|
||||
const { testValueValid, formattedResultExample: initialExample } =
|
||||
useTransSchema(schema);
|
||||
const { mock_set_id, tool_id } = useParams<DynamicParams>();
|
||||
|
||||
// space信息
|
||||
const spaceType = useSpaceStore(store => store.space.space_type);
|
||||
const isPersonal = spaceType === SpaceType.Personal;
|
||||
|
||||
const basicParams: PluginMockSetCommonParams = {
|
||||
environment: getEnvironment(),
|
||||
workspace_id: bizCtx.bizSpaceID || '',
|
||||
workspace_type: isPersonal ? 'personal_workspace' : 'team_workspace',
|
||||
tool_id: tool_id || '',
|
||||
mock_set_id: mock_set_id || '',
|
||||
};
|
||||
|
||||
const { save, loading } = useSaveMockData({
|
||||
mockSetId: mock_set_id,
|
||||
basicParams,
|
||||
bizCtx,
|
||||
onSuccess,
|
||||
});
|
||||
|
||||
const confirmHandler = () => {
|
||||
const values = editorsRef.current?.getValue();
|
||||
if (!values) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const value of values) {
|
||||
if (!value) {
|
||||
Toast.error('no data');
|
||||
return;
|
||||
}
|
||||
|
||||
if (calcStringSize(value) > MAX_SUBMIT_LENGTH) {
|
||||
Toast.error({
|
||||
content: I18n.t('mockset_toast_data_size_limit'),
|
||||
showClose: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!testValueValid(value)) {
|
||||
Toast.error({
|
||||
content: I18n.t('mockdata_field_empty', {
|
||||
fieldName: PRE_DEFINED_NO_EMPTY_KEY,
|
||||
}),
|
||||
showClose: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const mockDataId = String(mockInfo?.mock?.id || 0);
|
||||
|
||||
save(values, mockDataId);
|
||||
};
|
||||
|
||||
const validateHandler = (isValid: boolean[]) => {
|
||||
setDisableSubmit(isValid.some(v => !v));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
const unloadHandler = e => {
|
||||
const info = I18n.t('mockset_tip_data_will_lose');
|
||||
e.preventDefault();
|
||||
e.returnValue = info;
|
||||
return info;
|
||||
};
|
||||
|
||||
if (
|
||||
(mode === CreationMode.MODAL && visible) ||
|
||||
mode === CreationMode.CARD
|
||||
) {
|
||||
window.addEventListener('beforeunload', unloadHandler);
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('beforeunload', unloadHandler);
|
||||
};
|
||||
}, [mode, visible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (forceGenerate) {
|
||||
editorsRef.current?.forceStartGenerate?.(
|
||||
forceGenerate.mode,
|
||||
forceGenerate.count,
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return mode === CreationMode.MODAL ? (
|
||||
<UIModal
|
||||
visible={visible}
|
||||
title={
|
||||
mockInfo?.mock ? I18n.t('edit_mock_data') : I18n.t('add_mock_data')
|
||||
}
|
||||
className={s['mock-creation-modal']}
|
||||
keepDOM={false}
|
||||
footer={
|
||||
<>
|
||||
<span className="mr-[8px]">{I18n.t('mockset_save_description')}</span>
|
||||
<Divider layout="vertical" margin="0px" />
|
||||
<UIButton type={'tertiary'} key="Cancel" onClick={onCancel}>
|
||||
{I18n.t('cancel')}
|
||||
</UIButton>
|
||||
<UIButton
|
||||
type={'primary'}
|
||||
theme={'solid'}
|
||||
key="Confirm"
|
||||
onClick={confirmHandler}
|
||||
loading={loading}
|
||||
disabled={disableSubmit || disableSubmitWhenGenerating}
|
||||
>
|
||||
{I18n.t('confirm')}
|
||||
</UIButton>
|
||||
</>
|
||||
}
|
||||
width={1000}
|
||||
maskClosable={false}
|
||||
onCancel={onCancel}
|
||||
>
|
||||
<MocksetEditor
|
||||
className={s['mock-creation-modal-editor']}
|
||||
mockInfo={{
|
||||
mergedResultExample: initialExample,
|
||||
...mockInfo,
|
||||
}}
|
||||
readOnly={false}
|
||||
ref={editorsRef}
|
||||
onValidate={validateHandler}
|
||||
environment={{
|
||||
spaceId: bizCtx.bizSpaceID,
|
||||
mockSetId: mock_set_id,
|
||||
basicParams,
|
||||
}}
|
||||
isCreateScene={!mockInfo?.mock}
|
||||
onGenerationStatusChange={isGenerating =>
|
||||
setDisableSubmitWhenGenerating(isGenerating)
|
||||
}
|
||||
/>
|
||||
</UIModal>
|
||||
) : (
|
||||
<div className={s['mock-creation-card']}>
|
||||
<div className={s['mock-creation-card-editor']}>
|
||||
<MocksetEditor
|
||||
mockInfo={{
|
||||
mergedResultExample: initialExample,
|
||||
...mockInfo,
|
||||
}}
|
||||
ref={editorsRef}
|
||||
onValidate={validateHandler}
|
||||
environment={{
|
||||
spaceId: bizCtx.bizSpaceID,
|
||||
mockSetId: mock_set_id,
|
||||
basicParams,
|
||||
}}
|
||||
isCreateScene={!mockInfo?.mock}
|
||||
onGenerationStatusChange={isGenerating =>
|
||||
setDisableSubmitWhenGenerating(isGenerating)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className={s['mock-creation-card-operation']}>
|
||||
<Space>
|
||||
<Typography.Text>
|
||||
{I18n.t('mockset_save_description')}
|
||||
</Typography.Text>
|
||||
<Divider layout="vertical" margin="0px" />
|
||||
<UIButton
|
||||
type={'primary'}
|
||||
theme={'solid'}
|
||||
onClick={confirmHandler}
|
||||
loading={loading}
|
||||
disabled={disableSubmit || disableSubmitWhenGenerating}
|
||||
>
|
||||
{I18n.t('mockset_save')}
|
||||
</UIButton>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
/*
|
||||
* 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 { useParams } from 'react-router-dom';
|
||||
import {
|
||||
useState,
|
||||
useEffect,
|
||||
forwardRef,
|
||||
type ForwardedRef,
|
||||
useImperativeHandle,
|
||||
} from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { logger } from '@coze-arch/logger';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { type DynamicParams } from '@coze-arch/bot-typings/teamspace';
|
||||
import {
|
||||
EVENT_NAMES,
|
||||
sendTeaEvent,
|
||||
type ParamsTypeDefine,
|
||||
} from '@coze-arch/bot-tea';
|
||||
import { useSpaceStore } from '@coze-arch/bot-studio-store';
|
||||
import { Empty, Spin, UIModal } from '@coze-arch/bot-semi';
|
||||
import { PageType, usePageJumpResponse } from '@coze-arch/bot-hooks';
|
||||
import { SpaceType } from '@coze-arch/bot-api/developer_api';
|
||||
import { infra, type MockRule } from '@coze-arch/bot-api/debugger_api';
|
||||
import { debuggerApi } from '@coze-arch/bot-api';
|
||||
import {
|
||||
IllustrationNoContent,
|
||||
IllustrationNoContentDark,
|
||||
} from '@douyinfe/semi-illustrations';
|
||||
import { IconAlertCircle } from '@douyinfe/semi-icons';
|
||||
import { getEnvironment } from '@coze-studio/mockset-shared';
|
||||
|
||||
import { type MockDataInfo } from '../util/typings';
|
||||
import { SpaceHolder } from './space-holder';
|
||||
import { CreationMode, MockDataCreateCard } from './mock-data-create-card';
|
||||
import { MockDataCard } from './mock-data-card';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
enum RuleActions {
|
||||
CREATE,
|
||||
EDIT,
|
||||
DELETE,
|
||||
}
|
||||
|
||||
interface MockDataListProps {
|
||||
mockSetID?: string;
|
||||
perm: {
|
||||
readOnly: boolean;
|
||||
uninitialized: boolean;
|
||||
};
|
||||
toolSchema: string;
|
||||
bizCtx: infra.BizCtx;
|
||||
onListUpdate?: (length: number, needScrollToTop?: boolean) => void;
|
||||
}
|
||||
|
||||
export interface MockDataListActions {
|
||||
update: () => void;
|
||||
create: () => void;
|
||||
}
|
||||
|
||||
export const MockDataList = forwardRef(
|
||||
(
|
||||
{ mockSetID, perm, toolSchema, bizCtx, onListUpdate }: MockDataListProps,
|
||||
ref: ForwardedRef<MockDataListActions>,
|
||||
) => {
|
||||
// loading
|
||||
const [loading, setLoading] = useState(false);
|
||||
// mock data list
|
||||
const [mockDataList, setMockDataList] = useState<MockRule[]>([]);
|
||||
// modal visible
|
||||
const [createModalVisible, setCreateModalVisible] =
|
||||
useState<boolean>(false);
|
||||
// delete modal visible
|
||||
const [deleteModalVisible, setDeleteModalVisible] =
|
||||
useState<boolean>(false);
|
||||
const [deleting, setDeleting] = useState<boolean>(false);
|
||||
// 当前选中状态
|
||||
const [currentSelect, setCurrentSelect] = useState<
|
||||
MockDataInfo | undefined
|
||||
>();
|
||||
|
||||
const routeResponse = usePageJumpResponse(PageType.PLUGIN_MOCK_DATA);
|
||||
|
||||
const { mock_set_id, space_id, tool_id } = useParams<DynamicParams>();
|
||||
// space信息
|
||||
const spaceType = useSpaceStore(store => store.space.space_type);
|
||||
const isPersonal = spaceType === SpaceType.Personal;
|
||||
|
||||
const clickItemUpdateEntryHandler = (params: MockDataInfo) => {
|
||||
setCurrentSelect(params);
|
||||
setCreateModalVisible(true);
|
||||
};
|
||||
|
||||
const clickItemDeleteEntryHandler = (params: MockDataInfo) => {
|
||||
setCurrentSelect(params);
|
||||
setDeleteModalVisible(true);
|
||||
};
|
||||
|
||||
// 获取当前 mock set 下的 mock data
|
||||
const getMockData = async (needScrollToTop?: boolean) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await debuggerApi.MGetMockRule({
|
||||
bizCtx,
|
||||
mockSetID: mock_set_id,
|
||||
orderBy: infra.OrderBy.UpdateTime,
|
||||
desc: true,
|
||||
});
|
||||
setMockDataList(data.mockRules || []);
|
||||
|
||||
onListUpdate?.(data.mockRules?.length || 0, needScrollToTop);
|
||||
} catch (error) {
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
logger.error({ error, eventName: 'get_mock_data_fail' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteConfirmHandler = async () => {
|
||||
const { mock } = currentSelect || {};
|
||||
if (!mock) {
|
||||
return;
|
||||
}
|
||||
|
||||
const basicParams: Omit<
|
||||
ParamsTypeDefine[EVENT_NAMES.del_mock_front],
|
||||
'status'
|
||||
> = {
|
||||
environment: getEnvironment(),
|
||||
workspace_id: space_id || '',
|
||||
workspace_type: isPersonal ? 'personal_workspace' : 'team_workspace',
|
||||
tool_id: tool_id || '',
|
||||
mock_set_id: mock_set_id || '',
|
||||
mock_counts: 1,
|
||||
};
|
||||
|
||||
try {
|
||||
setDeleting(true);
|
||||
await debuggerApi.DeleteMockRule({
|
||||
bizCtx,
|
||||
id: String(mock.id),
|
||||
});
|
||||
|
||||
updateList(mock, RuleActions.DELETE);
|
||||
setCurrentSelect(undefined);
|
||||
setDeleteModalVisible(false);
|
||||
|
||||
sendTeaEvent(EVENT_NAMES.del_mock_front, {
|
||||
...basicParams,
|
||||
status: 0,
|
||||
});
|
||||
} catch (error) {
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
logger.error({ error, eventName: 'delete_mock_fail' });
|
||||
sendTeaEvent(EVENT_NAMES.del_mock_front, {
|
||||
...basicParams,
|
||||
status: 1,
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
error,
|
||||
});
|
||||
} finally {
|
||||
setDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 前端更新
|
||||
const updateList = (data: MockRule, action: RuleActions) => {
|
||||
let len = 0;
|
||||
if (action === RuleActions.CREATE) {
|
||||
// 创建场景直接 force update
|
||||
getMockData(true);
|
||||
} else if (action === RuleActions.DELETE) {
|
||||
len = mockDataList.length - 1;
|
||||
setMockDataList(cur => {
|
||||
const index = cur.findIndex(item => item.id === data?.id);
|
||||
if (index !== -1) {
|
||||
cur.splice(index, 1);
|
||||
}
|
||||
len = cur.length;
|
||||
return [...cur];
|
||||
});
|
||||
|
||||
onListUpdate?.(len);
|
||||
} else {
|
||||
len = mockDataList.length;
|
||||
setMockDataList(cur => {
|
||||
const index = cur.findIndex(item => item.id === data?.id);
|
||||
if (index !== -1) {
|
||||
cur.splice(index, 1);
|
||||
cur.unshift(data);
|
||||
}
|
||||
len = cur.length;
|
||||
return [...cur];
|
||||
});
|
||||
onListUpdate?.(len, true);
|
||||
}
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
update: getMockData,
|
||||
create: () => {
|
||||
setCurrentSelect(undefined);
|
||||
setCreateModalVisible(true);
|
||||
},
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
getMockData();
|
||||
}, [mockSetID]);
|
||||
|
||||
useEffect(() => {
|
||||
if (routeResponse?.generationMode) {
|
||||
// 清除跳转参数
|
||||
const state = {
|
||||
...history.state,
|
||||
usr: { ...(history.state.usr || {}), generationMode: undefined },
|
||||
};
|
||||
history.replaceState(state, '');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const renderList = () => {
|
||||
if (loading || perm.uninitialized) {
|
||||
return (
|
||||
<div className={s['list-container-no-header_flexible']}>
|
||||
<Spin
|
||||
size="large"
|
||||
spinning
|
||||
style={{ height: '80%', width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (perm.readOnly && mockDataList.length === 0) {
|
||||
return (
|
||||
<>
|
||||
<h1 className={classNames(s['content-title'])}>
|
||||
{I18n.t('mockset_data')}
|
||||
</h1>
|
||||
<div className={s['list-container_flexible']}>
|
||||
<Empty
|
||||
className={s.empty}
|
||||
image={<IllustrationNoContent />}
|
||||
darkModeImage={<IllustrationNoContentDark />}
|
||||
description={I18n.t('no_mock_yet')}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (!perm.readOnly && mockDataList.length === 0) {
|
||||
return (
|
||||
<div className={s['list-container-no-header_flexible']}>
|
||||
<MockDataCreateCard
|
||||
mode={CreationMode.CARD}
|
||||
mockInfo={{
|
||||
schema: toolSchema,
|
||||
}}
|
||||
onSuccess={data => {
|
||||
data && updateList(data[0], RuleActions.CREATE);
|
||||
}}
|
||||
bizCtx={bizCtx}
|
||||
forceGenerate={
|
||||
routeResponse?.generationMode
|
||||
? {
|
||||
mode: routeResponse.generationMode,
|
||||
count: 1,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className={classNames(s['content-title'])}>
|
||||
{I18n.t('mockset_data')}
|
||||
</h1>
|
||||
<div className={s['list-container_scroll']}>
|
||||
{mockDataList.map(item => (
|
||||
<MockDataCard
|
||||
readOnly={perm.readOnly}
|
||||
key={item.id}
|
||||
mock={item}
|
||||
schema={toolSchema}
|
||||
onEdit={params => clickItemUpdateEntryHandler(params)}
|
||||
onRemove={params => clickItemDeleteEntryHandler(params)}
|
||||
bizCtx={bizCtx}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SpaceHolder height={24} />
|
||||
{renderList()}
|
||||
<MockDataCreateCard
|
||||
mode={CreationMode.MODAL}
|
||||
mockInfo={
|
||||
currentSelect || {
|
||||
schema: toolSchema,
|
||||
}
|
||||
}
|
||||
visible={createModalVisible}
|
||||
onCancel={() => {
|
||||
setCurrentSelect(undefined);
|
||||
setCreateModalVisible(false);
|
||||
}}
|
||||
onSuccess={data => {
|
||||
setCurrentSelect(undefined);
|
||||
setCreateModalVisible(false);
|
||||
data?.[0] &&
|
||||
updateList(
|
||||
data[0],
|
||||
currentSelect ? RuleActions.EDIT : RuleActions.CREATE,
|
||||
);
|
||||
}}
|
||||
bizCtx={bizCtx}
|
||||
/>
|
||||
<UIModal
|
||||
type="info"
|
||||
icon={
|
||||
<IconAlertCircle
|
||||
size="extra-large"
|
||||
className="inline-flex text-[#FF2710]"
|
||||
/>
|
||||
}
|
||||
title={I18n.t('delete_mock_data')}
|
||||
visible={deleteModalVisible}
|
||||
onCancel={() => {
|
||||
setCurrentSelect(undefined);
|
||||
setDeleteModalVisible(false);
|
||||
}}
|
||||
okText={I18n.t('confirm')}
|
||||
cancelText={I18n.t('cancel')}
|
||||
confirmLoading={deleting}
|
||||
onOk={() => deleteConfirmHandler()}
|
||||
okType="danger"
|
||||
>
|
||||
{I18n.t('operation_cannot_be_reversed')}
|
||||
</UIModal>
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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 { useEffect, useState } from 'react';
|
||||
|
||||
import { UIBreadcrumb } from '@coze-studio/components';
|
||||
import { logger } from '@coze-arch/logger';
|
||||
import { UILayout } from '@coze-arch/bot-semi';
|
||||
import { usePageJumpResponse, PageType } from '@coze-arch/bot-hooks';
|
||||
import {
|
||||
type PluginMetaInfo,
|
||||
type PluginAPIInfo,
|
||||
} from '@coze-arch/bot-api/developer_api';
|
||||
import { type MockSet } from '@coze-arch/bot-api/debugger_api';
|
||||
import { DeveloperApi } from '@coze-arch/bot-api';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
interface MockSetPageBreadcrumbProps {
|
||||
pluginId?: string;
|
||||
apiInfo?: PluginAPIInfo;
|
||||
mockSetInfo?: MockSet;
|
||||
}
|
||||
|
||||
export function MockSetPageBreadcrumb({
|
||||
pluginId,
|
||||
apiInfo,
|
||||
mockSetInfo,
|
||||
}: MockSetPageBreadcrumbProps) {
|
||||
const routeResponse = usePageJumpResponse(PageType.PLUGIN_MOCK_DATA);
|
||||
|
||||
// 插件详情
|
||||
const [pluginInfo, setPluginInfo] = useState<PluginMetaInfo>({
|
||||
name: routeResponse?.pluginName,
|
||||
});
|
||||
|
||||
// 获取当前 plugin 信息
|
||||
const getPluginInfo = async () => {
|
||||
try {
|
||||
const res = await DeveloperApi.GetPluginInfo(
|
||||
{
|
||||
plugin_id: pluginId || '',
|
||||
},
|
||||
{ __disableErrorToast: true },
|
||||
);
|
||||
if (res?.code === 0) {
|
||||
setPluginInfo(res.meta_info || {});
|
||||
}
|
||||
} catch (error) {
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
logger.error({ error, eventName: 'get_plugin_info_fail' });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getPluginInfo();
|
||||
}, [pluginId]);
|
||||
|
||||
return (
|
||||
<UILayout.Header
|
||||
className={s['layout-header']}
|
||||
breadcrumb={
|
||||
<UIBreadcrumb
|
||||
showTooltip={{ width: '300px' }}
|
||||
pluginInfo={pluginInfo}
|
||||
pluginToolInfo={apiInfo}
|
||||
mockSetInfo={mockSetInfo}
|
||||
compact={false}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
.select-container {
|
||||
display: inline-block;
|
||||
|
||||
max-width: 100%;
|
||||
padding: 4px 6px;
|
||||
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
|
||||
&.switch-disabled {
|
||||
* {
|
||||
/* stylelint-disable-next-line declaration-no-important */
|
||||
color: #1D1C23CC !important;
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
span {
|
||||
&[aria-label="small_triangle_down"] {
|
||||
color: #1D1C23CC;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.option-list {
|
||||
min-width: 234px;
|
||||
max-width: 336px;
|
||||
padding: 4px;
|
||||
font-size: 12px;
|
||||
|
||||
:global {
|
||||
.semi-typography {
|
||||
color: #1D1C23;
|
||||
|
||||
}
|
||||
|
||||
.semi-select-option-list {
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.semi-select-loading-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-selected {
|
||||
overflow: hidden;
|
||||
|
||||
width: 100%;
|
||||
height: 16px;
|
||||
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.select-label {
|
||||
svg {
|
||||
>path {
|
||||
/* stylelint-disable-next-line declaration-no-important */
|
||||
fill: currentcolor !important;
|
||||
fill-opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-option-render-focused {
|
||||
cursor: pointer;
|
||||
background-color: rgb(46 46 56 / 8%);
|
||||
|
||||
}
|
||||
|
||||
.custom-option-render-selected {
|
||||
font-weight: 600;
|
||||
|
||||
}
|
||||
|
||||
.custom-option-render-disabled {
|
||||
cursor: not-allowed;
|
||||
|
||||
:global {
|
||||
.semi-typography {
|
||||
color: #1D1C2359;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.select-option-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
height: 32px;
|
||||
padding: 8px 8px 8px 16px;
|
||||
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
margin: 4px 0;
|
||||
background: var(--semi-color-border);
|
||||
}
|
||||
|
||||
.create-container {
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
height: 32px;
|
||||
padding: 0 16px;
|
||||
|
||||
color: #4D53E8;
|
||||
}
|
||||
|
||||
.load-more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #4D53E8;
|
||||
|
||||
.spin-icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,579 @@
|
||||
/*
|
||||
* 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 */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable max-lines */
|
||||
/* eslint-disable max-lines-per-function */
|
||||
import {
|
||||
useState,
|
||||
useEffect,
|
||||
useRef,
|
||||
forwardRef,
|
||||
type ForwardedRef,
|
||||
useImperativeHandle,
|
||||
} from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
useInViewport,
|
||||
useInfiniteScroll,
|
||||
useRequest,
|
||||
useUnmount,
|
||||
} from 'ahooks';
|
||||
import { userStoreService } from '@coze-studio/user-store';
|
||||
import { logger } from '@coze-arch/logger';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import {
|
||||
EVENT_NAMES,
|
||||
type ParamsTypeDefine,
|
||||
sendTeaEvent,
|
||||
} from '@coze-arch/bot-tea';
|
||||
import { useSpaceStore } from '@coze-arch/bot-studio-store';
|
||||
// eslint-disable-next-line @coze-arch/no-pkg-dir-import
|
||||
import { type SemiSelectActions } from '@coze-arch/bot-semi/src/components/ui-select';
|
||||
import { Spin, Tooltip, UIButton, UISelect } from '@coze-arch/bot-semi';
|
||||
import { IconAdd } from '@coze-arch/bot-icons';
|
||||
import { SceneType, usePageJumpService } from '@coze-arch/bot-hooks';
|
||||
import { SpaceType } from '@coze-arch/bot-api/developer_api';
|
||||
import {
|
||||
TrafficScene,
|
||||
infra,
|
||||
type BizCtx,
|
||||
type MockSet,
|
||||
} from '@coze-arch/bot-api/debugger_api';
|
||||
import { debuggerApi } from '@coze-arch/bot-api';
|
||||
import { IconTick, IconUploadError } from '@douyinfe/semi-icons';
|
||||
import {
|
||||
type MockSelectOptionProps,
|
||||
type MockSelectRenderOptionProps,
|
||||
type MockSetSelectProps,
|
||||
MockSetStatus,
|
||||
getEnvironment,
|
||||
getMockSubjectInfo,
|
||||
getPluginInfo,
|
||||
} from '@coze-studio/mockset-shared';
|
||||
import {
|
||||
builtinSuccessCallback,
|
||||
MockSetEditModal,
|
||||
} from '@coze-studio/mockset-edit-modal-adapter';
|
||||
|
||||
import { MockSetDeleteModal } from '../mockset-delete-modal';
|
||||
import { useInitialGetEnabledMockSet } from '../hooks/use-get-mockset';
|
||||
import {
|
||||
CONNECTOR_ID,
|
||||
DELAY_TIME,
|
||||
MOCK_OPTION_LIST,
|
||||
POLLING_INTERVAL,
|
||||
REAL_DATA_ID,
|
||||
REAL_DATA_MOCKSET,
|
||||
} from '../const';
|
||||
import { getUsedScene, isCurrent, isRealData } from '../../util';
|
||||
import { MockSetItem } from './option-item';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export function getMockSetOption(mockSet: MockSet): MockSelectOptionProps {
|
||||
const isInValid =
|
||||
!isRealData(mockSet) &&
|
||||
(mockSet?.schemaIncompatible || !mockSet?.mockRuleQuantity);
|
||||
return {
|
||||
value: mockSet?.id || '',
|
||||
label: (
|
||||
<Tooltip
|
||||
key={mockSet?.id}
|
||||
content={I18n.t('mockset_invaild_tip', { MockSetName: mockSet.name })}
|
||||
style={{ display: isInValid ? 'block' : 'none' }}
|
||||
>
|
||||
<span
|
||||
className={classNames(
|
||||
'flex items-center w-[100%] min-w-0',
|
||||
styles['select-label'],
|
||||
)}
|
||||
>
|
||||
{isInValid ? (
|
||||
<IconUploadError
|
||||
style={{
|
||||
verticalAlign: 'middle',
|
||||
marginRight: 2,
|
||||
color: '#FF8500',
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<span
|
||||
className={classNames(
|
||||
'flex-1 min-w-0 overflow-hidden text-ellipsis',
|
||||
isInValid ? 'text-[#1D1C2359]' : 'text-[#1D1C23CC]',
|
||||
)}
|
||||
>
|
||||
{mockSet?.name || ''}
|
||||
</span>
|
||||
</span>
|
||||
</Tooltip>
|
||||
),
|
||||
disabled: isInValid,
|
||||
detail: mockSet,
|
||||
};
|
||||
}
|
||||
|
||||
export function getMockSetOptionList(
|
||||
mockSets: MockSet[],
|
||||
): Array<MockSelectOptionProps> {
|
||||
return mockSets.map(mockSet => getMockSetOption(mockSet));
|
||||
}
|
||||
export interface MockSetSelectActions {
|
||||
handleParentNodeDelete: () => void;
|
||||
}
|
||||
|
||||
const MockSetSelectComp = (
|
||||
{
|
||||
bindSubjectInfo: mockSubjectInfo,
|
||||
bizCtx: bizSceneCtx,
|
||||
className,
|
||||
style: baseStyle,
|
||||
readonly,
|
||||
}: MockSetSelectProps,
|
||||
ref: ForwardedRef<MockSetSelectActions>,
|
||||
) => {
|
||||
const { detail: subjectDetail, ...bindSubjectInfo } = mockSubjectInfo;
|
||||
|
||||
const { spaceID, toolID, pluginID } = getPluginInfo(
|
||||
bizSceneCtx,
|
||||
bindSubjectInfo,
|
||||
);
|
||||
const uid = userStoreService.useUserInfo()?.user_id_str;
|
||||
|
||||
const spaceType = useSpaceStore(s => s.space.space_type);
|
||||
const isPersonal = spaceType === SpaceType.Personal;
|
||||
|
||||
const bizCtx: BizCtx = {
|
||||
...bizSceneCtx,
|
||||
connectorUID: uid,
|
||||
connectorID: CONNECTOR_ID, // 业务线为Coze
|
||||
};
|
||||
|
||||
const { jump } = usePageJumpService();
|
||||
|
||||
const [selectedMockSet, setSelectedMockSet] =
|
||||
useState<MockSet>(REAL_DATA_MOCKSET);
|
||||
const selectedValue = getMockSetOption(selectedMockSet);
|
||||
|
||||
const [optionList, setOptionList] = useState<Array<MockSelectOptionProps>>(
|
||||
getMockSetOptionList(MOCK_OPTION_LIST),
|
||||
);
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [deleteMockSet, setDeleteMockSet] = useState<MockSet | undefined>();
|
||||
|
||||
const preSelectionRef = useRef<MockSet>(REAL_DATA_MOCKSET);
|
||||
const selectionDomRef = useRef<HTMLDivElement>(null);
|
||||
const selectionRef = useRef<SemiSelectActions>(null);
|
||||
|
||||
const [inViewPort] = useInViewport(selectionDomRef);
|
||||
const {
|
||||
data: enabledMockSetInfo,
|
||||
addMockComp,
|
||||
removeMockComp,
|
||||
start,
|
||||
cancel,
|
||||
setRestartTimer,
|
||||
} = useInitialGetEnabledMockSet({
|
||||
bizCtx,
|
||||
pollingInterval: POLLING_INTERVAL,
|
||||
});
|
||||
|
||||
const { runAsync: changeMockSet, loading: changeMockSetLoading } = useRequest(
|
||||
async (mockSet: MockSet, isBinding = true) => {
|
||||
const basicParams: ParamsTypeDefine[EVENT_NAMES.use_mockset_front] = {
|
||||
environment: getEnvironment(),
|
||||
workspace_id: spaceID || '',
|
||||
workspace_type: isPersonal ? 'personal_workspace' : 'team_workspace',
|
||||
tool_id: toolID || '',
|
||||
status: 1,
|
||||
mock_set_id: (mockSet.id as string) || '',
|
||||
where: getUsedScene(bizCtx.trafficScene),
|
||||
};
|
||||
try {
|
||||
await debuggerApi.BindMockSet({
|
||||
mockSetID: isBinding ? mockSet.id : '0',
|
||||
bizCtx,
|
||||
mockSubject: bindSubjectInfo,
|
||||
});
|
||||
isBinding &&
|
||||
sendTeaEvent(EVENT_NAMES.use_mockset_front, {
|
||||
...basicParams,
|
||||
status: 0,
|
||||
});
|
||||
} catch (e) {
|
||||
setSelectedMockSet(preSelectionRef.current);
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
logger.error({ error: e, eventName: 'change_mockset_fail' });
|
||||
isBinding &&
|
||||
sendTeaEvent(EVENT_NAMES.use_mockset_front, {
|
||||
...basicParams,
|
||||
status: 1,
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
error: e?.msg as string,
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
manual: true,
|
||||
},
|
||||
);
|
||||
|
||||
const handleChange = async (obj?: MockSelectOptionProps) => {
|
||||
cancel();
|
||||
preSelectionRef.current = selectedMockSet;
|
||||
setSelectedMockSet((obj as MockSelectOptionProps)?.detail || {});
|
||||
await changeMockSet((obj as MockSelectOptionProps)?.detail || {});
|
||||
const restartTimerId = setTimeout(() => {
|
||||
start();
|
||||
}, DELAY_TIME);
|
||||
setRestartTimer(restartTimerId);
|
||||
};
|
||||
|
||||
const {
|
||||
reload: fetchOptionList,
|
||||
loadMore,
|
||||
loading,
|
||||
loadingMore,
|
||||
data: optionListData,
|
||||
} = useInfiniteScroll(
|
||||
async d => {
|
||||
try {
|
||||
const res = await debuggerApi.MGetMockSet({
|
||||
bizCtx,
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
mockSubject: getMockSubjectInfo(bizCtx, mockSubjectInfo),
|
||||
pageToken: d?.pageToken,
|
||||
orderBy: infra.OrderBy.UpdateTime,
|
||||
desc: true,
|
||||
});
|
||||
const mockSetList = getMockSetOptionList(res?.mockSets || []);
|
||||
|
||||
return {
|
||||
list: mockSetList || [],
|
||||
pageToken: res?.pageToken,
|
||||
hasMore: res?.hasMore ?? true,
|
||||
schema: res?.schema,
|
||||
count: res?.count,
|
||||
};
|
||||
} catch (e) {
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
logger.error({ error: e, eventName: 'mockset_list_fetch_fail' });
|
||||
return {
|
||||
list: [],
|
||||
pageToken: d?.pageToken,
|
||||
hasMore: d?.hasMore,
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
manual: true,
|
||||
},
|
||||
);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleParentNodeDelete: () => {
|
||||
changeMockSet(selectedMockSet, false);
|
||||
},
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
const newOptionList = [
|
||||
getMockSetOption(REAL_DATA_MOCKSET),
|
||||
...(optionListData?.list || []),
|
||||
];
|
||||
setOptionList(newOptionList);
|
||||
}, [optionListData]);
|
||||
|
||||
useEffect(() => {
|
||||
const mockSetInfo = enabledMockSetInfo.find(mockInfo =>
|
||||
isCurrent(
|
||||
{
|
||||
bizCtx: mockInfo?.mockSetBinding?.bizCtx || {},
|
||||
bindSubjectInfo: mockInfo?.mockSetBinding?.mockSubject || {},
|
||||
},
|
||||
{
|
||||
bizCtx,
|
||||
bindSubjectInfo,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (changeMockSetLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mockSetInfo?.mockSetDetail) {
|
||||
setSelectedMockSet(mockSetInfo?.mockSetDetail);
|
||||
} else {
|
||||
setSelectedMockSet(REAL_DATA_MOCKSET);
|
||||
}
|
||||
}, [enabledMockSetInfo]);
|
||||
|
||||
useUnmount(() => {
|
||||
const length = removeMockComp({ bizCtx, bindSubjectInfo });
|
||||
if (!length) {
|
||||
cancel();
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (inViewPort) {
|
||||
const length = addMockComp({ bizCtx, bindSubjectInfo });
|
||||
if (length === 1) {
|
||||
start();
|
||||
}
|
||||
} else {
|
||||
const length = removeMockComp({ bizCtx, bindSubjectInfo });
|
||||
if (!length) {
|
||||
cancel();
|
||||
}
|
||||
}
|
||||
}, [inViewPort]);
|
||||
|
||||
const closePanel = () => {
|
||||
selectionRef?.current?.close();
|
||||
};
|
||||
|
||||
const handleView = (
|
||||
record?: MockSet,
|
||||
autoGenerateConfig?: { generateMode: number },
|
||||
) => {
|
||||
const { trafficScene } = bizCtx || {};
|
||||
const { id } = record || {};
|
||||
if (spaceID && pluginID && toolID && id) {
|
||||
jump(
|
||||
trafficScene === TrafficScene.CozeWorkflowDebug
|
||||
? SceneType.WORKFLOW__TO__PLUGIN_MOCK_DATA
|
||||
: SceneType.BOT__TO__PLUGIN_MOCK_DATA,
|
||||
{
|
||||
spaceId: spaceID,
|
||||
pluginId: pluginID,
|
||||
toolId: toolID,
|
||||
toolName: subjectDetail?.name,
|
||||
mockSetId: String(id),
|
||||
mockSetName: record?.name,
|
||||
bizCtx: JSON.stringify(bizCtx),
|
||||
bindSubjectInfo: JSON.stringify(bindSubjectInfo),
|
||||
generationMode: autoGenerateConfig?.generateMode,
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const renderCreateMockSet = () => (
|
||||
<>
|
||||
<div className={styles.divider}></div>
|
||||
<div
|
||||
onClick={() => {
|
||||
setShowCreateModal(true);
|
||||
closePanel();
|
||||
}}
|
||||
className={styles['create-container']}
|
||||
>
|
||||
<IconAdd
|
||||
className="mr-[10px]"
|
||||
style={{ fontSize: 14, color: '#4D53E8' }}
|
||||
/>
|
||||
<span>{I18n.t('create_mockset')}</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const renderLoadMore = () =>
|
||||
loading ||
|
||||
(optionListData?.list?.length || 0) >=
|
||||
(optionListData?.count || 0) ? null : (
|
||||
<div
|
||||
className={classNames(
|
||||
styles['select-option-container'],
|
||||
styles['load-more'],
|
||||
)}
|
||||
>
|
||||
{loadingMore ? (
|
||||
<>
|
||||
<Spin wrapperClassName={styles['spin-icon']} />
|
||||
<span>{I18n.t('Loading')}</span>
|
||||
</>
|
||||
) : (
|
||||
<UIButton
|
||||
onClick={loadMore}
|
||||
theme="borderless"
|
||||
style={{ fontSize: 12 }}
|
||||
>
|
||||
{I18n.t('Load More' as any)}
|
||||
</UIButton>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderOptionItem = (renderProps: MockSelectRenderOptionProps) => {
|
||||
const {
|
||||
disabled,
|
||||
selected,
|
||||
value,
|
||||
focused,
|
||||
style,
|
||||
onMouseEnter,
|
||||
onClick,
|
||||
detail,
|
||||
} = renderProps;
|
||||
|
||||
const getTooltipInfo = () => {
|
||||
if (detail?.schemaIncompatible) {
|
||||
return I18n.t('tool_updated_check_mockset_compatibility');
|
||||
} else if ((detail?.mockRuleQuantity || 0) <= 0) {
|
||||
return I18n.t('mockset_is_empty_add_data_before_use');
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
zIndex={110}
|
||||
content={getTooltipInfo()}
|
||||
visible={disabled && focused}
|
||||
position="left"
|
||||
style={{ display: disabled ? 'block' : 'none' }} // visible disabled不生效
|
||||
>
|
||||
<div
|
||||
style={style}
|
||||
className={classNames(
|
||||
styles['select-option-container'],
|
||||
focused && styles['custom-option-render-focused'],
|
||||
disabled && styles['custom-option-render-disabled'],
|
||||
selected && styles['custom-option-render-selected'],
|
||||
)}
|
||||
onClick={onClick}
|
||||
onMouseEnter={onMouseEnter}
|
||||
>
|
||||
<div className="w-[16px] h-[16px] mr-[8px]">
|
||||
{selected ? (
|
||||
<IconTick style={{ fontSize: 14 }} className="text-[#4D53E8]" />
|
||||
) : (
|
||||
<div className="w-[16px]"></div>
|
||||
)}
|
||||
</div>
|
||||
{value === REAL_DATA_ID ? (
|
||||
<span>{I18n.t('real_data')}</span>
|
||||
) : (
|
||||
<MockSetItem
|
||||
status={
|
||||
detail?.schemaIncompatible
|
||||
? MockSetStatus.Incompatible
|
||||
: MockSetStatus.Normal
|
||||
}
|
||||
name={detail?.name || ''}
|
||||
onDelete={() => {
|
||||
closePanel();
|
||||
setDeleteMockSet(detail);
|
||||
}}
|
||||
onView={() => {
|
||||
handleView(detail);
|
||||
}}
|
||||
disableCreator={isPersonal}
|
||||
viewOnly={uid !== detail?.creator?.ID}
|
||||
creatorName={detail?.creator?.name}
|
||||
className="flex-1 min-w-0"
|
||||
></MockSetItem>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={selectionDomRef} style={baseStyle} className={className}>
|
||||
<UISelect
|
||||
zIndex={100}
|
||||
stopPropagation
|
||||
disabled={readonly || changeMockSetLoading}
|
||||
className={classNames(
|
||||
styles['select-container'],
|
||||
changeMockSetLoading && styles['switch-disabled'],
|
||||
)}
|
||||
ref={selectionRef}
|
||||
selectedClassname={styles['item-selected']}
|
||||
optionList={optionList}
|
||||
dropdownClassName={styles['option-list']}
|
||||
outerBottomSlot={renderCreateMockSet()}
|
||||
innerBottomSlot={renderLoadMore()}
|
||||
onDropdownVisibleChange={(visible: boolean) => {
|
||||
if (visible) {
|
||||
fetchOptionList();
|
||||
} else {
|
||||
setOptionList([getMockSetOption(REAL_DATA_MOCKSET)]);
|
||||
}
|
||||
}}
|
||||
loading={loading}
|
||||
renderOptionItem={renderOptionItem}
|
||||
value={selectedValue}
|
||||
onChangeWithObject
|
||||
onChange={async obj => {
|
||||
await handleChange(obj as unknown as MockSelectOptionProps);
|
||||
}}
|
||||
/>
|
||||
{showCreateModal ? (
|
||||
<MockSetEditModal
|
||||
zIndex={9999}
|
||||
visible={showCreateModal}
|
||||
onCancel={() => setShowCreateModal(false)}
|
||||
onSuccess={(info, config) => {
|
||||
const { id } = info || {};
|
||||
setShowCreateModal(false);
|
||||
|
||||
builtinSuccessCallback(config);
|
||||
|
||||
handleView({ id }, config);
|
||||
}}
|
||||
initialInfo={{
|
||||
bizCtx,
|
||||
bindSubjectInfo,
|
||||
name: subjectDetail?.name,
|
||||
}}
|
||||
needResetPopoverContainer={
|
||||
bizCtx.trafficScene === TrafficScene.CozeWorkflowDebug
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
{deleteMockSet ? (
|
||||
<MockSetDeleteModal
|
||||
zIndex={9999}
|
||||
visible={!!deleteMockSet}
|
||||
mockSetInfo={{
|
||||
detail: deleteMockSet,
|
||||
ctx: { bizCtx, mockSubjectInfo: bindSubjectInfo },
|
||||
}}
|
||||
onSuccess={() => {
|
||||
deleteMockSet.id === selectedMockSet.id &&
|
||||
setSelectedMockSet(REAL_DATA_MOCKSET);
|
||||
setDeleteMockSet(undefined);
|
||||
cancel();
|
||||
start();
|
||||
}}
|
||||
onCancel={() => setDeleteMockSet(undefined)}
|
||||
needResetPopoverContainer={
|
||||
bizCtx.trafficScene === TrafficScene.CozeWorkflowDebug
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const MockSetSelect = forwardRef(MockSetSelectComp);
|
||||
@@ -0,0 +1,49 @@
|
||||
.mock-select-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: '100%';
|
||||
}
|
||||
|
||||
.mock-main-info {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
|
||||
.status-icon {
|
||||
margin-right: 4px;
|
||||
color: #FF8500;
|
||||
}
|
||||
|
||||
.mock-name {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.mock-extra-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
width: 64px;
|
||||
margin-left: 16px;
|
||||
|
||||
.creator-name {
|
||||
font-size: 12px;
|
||||
color: #1D1C2359;
|
||||
}
|
||||
|
||||
.operation-icon {
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
width: 40px;
|
||||
|
||||
font-size: 14px;
|
||||
color: #1D1C2399;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* 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 { useState, type CSSProperties } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Typography, UIIconButton } from '@coze-arch/bot-semi';
|
||||
import { IconDeleteOutline, IconEdit } from '@coze-arch/bot-icons';
|
||||
import { IconEyeOpened, IconUploadError } from '@douyinfe/semi-icons';
|
||||
import { MockSetStatus } from '@coze-studio/mockset-shared';
|
||||
|
||||
import styles from './option-item.module.less';
|
||||
|
||||
export interface MockSetItemProps {
|
||||
name: string;
|
||||
onDelete?: () => void;
|
||||
onView?: () => void;
|
||||
status?: MockSetStatus;
|
||||
creatorName?: string;
|
||||
viewOnly?: boolean;
|
||||
disableCreator?: boolean;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
export const MockSetItem = ({
|
||||
name,
|
||||
onDelete,
|
||||
onView,
|
||||
status = MockSetStatus.Normal,
|
||||
creatorName,
|
||||
viewOnly,
|
||||
disableCreator,
|
||||
className,
|
||||
style,
|
||||
}: MockSetItemProps) => {
|
||||
const [isHover, setIsHover] = useState(false);
|
||||
const renderExtraInfo = () => {
|
||||
if (isHover) {
|
||||
return (
|
||||
<div
|
||||
className={styles['operation-icon']}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
{viewOnly ? (
|
||||
<UIIconButton onClick={onView} icon={<IconEyeOpened />} />
|
||||
) : (
|
||||
<>
|
||||
<UIIconButton
|
||||
onClick={onView}
|
||||
icon={<IconEdit />}
|
||||
wrapperClass="mr-[4px]"
|
||||
/>
|
||||
<UIIconButton onClick={onDelete} icon={<IconDeleteOutline />} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return disableCreator ? null : (
|
||||
<Typography.Text
|
||||
ellipsis={{
|
||||
showTooltip: {
|
||||
opts: { content: creatorName },
|
||||
},
|
||||
}}
|
||||
className={styles['creator-name']}
|
||||
>
|
||||
{creatorName}
|
||||
</Typography.Text>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles['mock-select-item'], className)}
|
||||
style={style}
|
||||
onMouseEnter={() => {
|
||||
setIsHover(true);
|
||||
}}
|
||||
onMouseLeave={() => setIsHover(false)}
|
||||
>
|
||||
<span className={styles['mock-main-info']}>
|
||||
{status !== MockSetStatus.Normal && (
|
||||
<IconUploadError className={styles['status-icon']} />
|
||||
)}
|
||||
<Typography.Text ellipsis={{}} className={styles['mock-name']}>
|
||||
{name}
|
||||
</Typography.Text>
|
||||
</span>
|
||||
<div className={styles['mock-extra-info']}>{renderExtraInfo()}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* 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 { useState } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Space, UIIconButton } from '@coze-arch/bot-semi';
|
||||
import { IconEditNew } from '@coze-arch/bot-icons';
|
||||
import { type infra, type MockSet } from '@coze-arch/bot-api/debugger_api';
|
||||
import { MockSetEditModal } from '@coze-studio/mockset-edit-modal-adapter';
|
||||
|
||||
import { LongTextWithTooltip } from './long-text-with-tooltip';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
interface MockSetIntroProps {
|
||||
isFullHeader: boolean;
|
||||
readOnly?: boolean;
|
||||
mockSetInfo: MockSet;
|
||||
onUpdateMockSetInfo?: (mockSetInfo?: MockSet) => void;
|
||||
bizCtx: infra.BizCtx;
|
||||
}
|
||||
|
||||
const GAP_2 = 2;
|
||||
const GAP_4 = 4;
|
||||
|
||||
export function MockSetIntro({
|
||||
isFullHeader = true,
|
||||
readOnly = true,
|
||||
mockSetInfo,
|
||||
onUpdateMockSetInfo,
|
||||
bizCtx,
|
||||
}: MockSetIntroProps) {
|
||||
const [showEditModal, setShowEditModal] = useState<boolean>(false);
|
||||
const editHandler = (info?: MockSet) => {
|
||||
onUpdateMockSetInfo?.(info);
|
||||
setShowEditModal(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Space
|
||||
spacing={isFullHeader ? GAP_2 : GAP_4}
|
||||
className={classNames(
|
||||
s['mock-set-intro-title'],
|
||||
isFullHeader ? s['mock-set-intro-title_full'] : '',
|
||||
)}
|
||||
>
|
||||
<LongTextWithTooltip
|
||||
className={classNames(
|
||||
s['mock-set-intro-name'],
|
||||
isFullHeader ? s['mock-set-intro-name_full'] : '',
|
||||
)}
|
||||
>
|
||||
{mockSetInfo.name}
|
||||
</LongTextWithTooltip>
|
||||
{!readOnly && mockSetInfo.name ? (
|
||||
<UIIconButton
|
||||
icon={
|
||||
<IconEditNew
|
||||
className={classNames(
|
||||
s['mock-set-intro-edit'],
|
||||
isFullHeader ? s['mock-set-intro-edit_full'] : '',
|
||||
)}
|
||||
/>
|
||||
}
|
||||
size="small"
|
||||
theme="borderless"
|
||||
onClick={() => setShowEditModal(true)}
|
||||
/>
|
||||
) : null}
|
||||
<MockSetEditModal
|
||||
visible={showEditModal}
|
||||
initialInfo={{
|
||||
bindSubjectInfo: mockSetInfo.mockSubject || {},
|
||||
bizCtx,
|
||||
id: String(mockSetInfo.id),
|
||||
name: mockSetInfo.name,
|
||||
desc: mockSetInfo.description,
|
||||
}}
|
||||
onSuccess={editHandler}
|
||||
onCancel={() => setShowEditModal(false)}
|
||||
></MockSetEditModal>
|
||||
</Space>
|
||||
|
||||
{mockSetInfo.description ? (
|
||||
<LongTextWithTooltip
|
||||
className={classNames(
|
||||
s['mock-set-intro-desc'],
|
||||
s['mock-set-intro-desc_priority'],
|
||||
isFullHeader ? s['mock-set-intro-desc_full'] : '',
|
||||
)}
|
||||
>
|
||||
{mockSetInfo.description}
|
||||
</LongTextWithTooltip>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* 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 { useEffect, useState } from 'react';
|
||||
|
||||
import { useRequest } from 'ahooks';
|
||||
import { logger } from '@coze-arch/logger';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import {
|
||||
EVENT_NAMES,
|
||||
type ParamsTypeDefine,
|
||||
sendTeaEvent,
|
||||
} from '@coze-arch/bot-tea';
|
||||
import { useSpaceStore } from '@coze-arch/bot-studio-store';
|
||||
import { UIModal } from '@coze-arch/bot-semi';
|
||||
import { SpaceType } from '@coze-arch/bot-api/developer_api';
|
||||
import {
|
||||
type BizCtx,
|
||||
type MockSet,
|
||||
type ComponentSubject,
|
||||
} from '@coze-arch/bot-api/debugger_api';
|
||||
import { debuggerApi } from '@coze-arch/bot-api';
|
||||
import { IconAlertCircle } from '@douyinfe/semi-icons';
|
||||
import { getEnvironment, getPluginInfo } from '@coze-studio/mockset-shared';
|
||||
|
||||
export interface MockSetInfo {
|
||||
detail: MockSet;
|
||||
ctx?: {
|
||||
mockSubjectInfo?: ComponentSubject;
|
||||
bizCtx?: BizCtx;
|
||||
};
|
||||
}
|
||||
|
||||
export interface MockSetEditModalProps {
|
||||
visible: boolean;
|
||||
zIndex?: number;
|
||||
mockSetInfo: MockSetInfo;
|
||||
onSuccess?: () => void;
|
||||
onCancel?: () => void;
|
||||
needResetPopoverContainer?: boolean;
|
||||
}
|
||||
|
||||
function isValidRefCount(refCount: number) {
|
||||
return refCount >= 0;
|
||||
}
|
||||
|
||||
export const MockSetDeleteModal = ({
|
||||
visible,
|
||||
mockSetInfo,
|
||||
onSuccess,
|
||||
onCancel,
|
||||
zIndex,
|
||||
needResetPopoverContainer,
|
||||
}: MockSetEditModalProps) => {
|
||||
const {
|
||||
detail: { id },
|
||||
ctx,
|
||||
} = mockSetInfo || {};
|
||||
const [mockSetRefCount, setMockSetRefCount] = useState(-1);
|
||||
|
||||
// space信息
|
||||
const spaceType = useSpaceStore(s => s.space.space_type);
|
||||
const isPersonal = spaceType === SpaceType.Personal;
|
||||
|
||||
const { run: fetchRefInfo } = useRequest(
|
||||
async () => {
|
||||
try {
|
||||
const { spaceID } = getPluginInfo(
|
||||
ctx?.bizCtx || {},
|
||||
ctx?.mockSubjectInfo || {},
|
||||
);
|
||||
const { usersUsageCount } = await debuggerApi.GetMockSetUsageInfo({
|
||||
mockSetID: id,
|
||||
spaceID,
|
||||
});
|
||||
setMockSetRefCount(Number(usersUsageCount ?? 0));
|
||||
} catch (e) {
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
logger.error({ error: e, eventName: 'fetch_mockset_ref_fail' });
|
||||
setMockSetRefCount(0);
|
||||
}
|
||||
},
|
||||
{
|
||||
manual: true,
|
||||
},
|
||||
);
|
||||
useEffect(() => {
|
||||
fetchRefInfo();
|
||||
}, [mockSetInfo]);
|
||||
|
||||
const renderTitle =
|
||||
mockSetRefCount > 0
|
||||
? I18n.t('people_using_mockset_delete', { num: mockSetRefCount })
|
||||
: I18n.t('delete_the_mockset');
|
||||
|
||||
const handleOk = async () => {
|
||||
const { toolID, spaceID } = getPluginInfo(
|
||||
ctx?.bizCtx || {},
|
||||
ctx?.mockSubjectInfo || {},
|
||||
);
|
||||
const basicParams: ParamsTypeDefine[EVENT_NAMES.del_mockset_front] = {
|
||||
environment: getEnvironment(),
|
||||
workspace_id: spaceID || '',
|
||||
workspace_type: isPersonal ? 'personal_workspace' : 'team_workspace',
|
||||
tool_id: toolID || '',
|
||||
mock_set_id: String(id) || '',
|
||||
status: 1,
|
||||
};
|
||||
try {
|
||||
id && (await debuggerApi.DeleteMockSet({ id, bizCtx: ctx?.bizCtx }));
|
||||
onSuccess?.();
|
||||
sendTeaEvent(EVENT_NAMES.del_mockset_front, {
|
||||
...basicParams,
|
||||
status: 0,
|
||||
});
|
||||
} catch (e) {
|
||||
sendTeaEvent(EVENT_NAMES.del_mockset_front, {
|
||||
...basicParams,
|
||||
status: 1,
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
error: e?.message as string,
|
||||
});
|
||||
}
|
||||
};
|
||||
return (
|
||||
<UIModal
|
||||
type="info"
|
||||
zIndex={zIndex}
|
||||
icon={
|
||||
<IconAlertCircle
|
||||
size="extra-large"
|
||||
className="inline-flex text-[#FF2710]"
|
||||
/>
|
||||
}
|
||||
title={renderTitle}
|
||||
visible={isValidRefCount(mockSetRefCount) && visible}
|
||||
onCancel={onCancel}
|
||||
onOk={handleOk}
|
||||
getPopupContainer={
|
||||
needResetPopoverContainer ? () => document.body : undefined
|
||||
}
|
||||
okType="danger"
|
||||
>
|
||||
{I18n.t('operation_cannot_be_reversed')}
|
||||
</UIModal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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 function SpaceHolder({
|
||||
height,
|
||||
width,
|
||||
}: {
|
||||
height?: number;
|
||||
width?: number;
|
||||
}) {
|
||||
return (
|
||||
<div style={{ width, height, display: width ? 'inline-block' : 'block' }} />
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user