feat(脑图): 测试规划脑图

This commit is contained in:
baiqi 2024-06-12 18:44:55 +08:00 committed by 刘瑞斌
parent 251f923b43
commit bfe7789e78
21 changed files with 792 additions and 398 deletions

View File

@ -40,7 +40,6 @@ import {
GetDefinitionDetailUrl,
GetDefinitionScheduleUrl,
GetDependencyUrl,
GetEnvListUrl,
GetEnvModuleUrl,
GetExecuteHistoryUrl,
GetMockUrlUrl,
@ -48,13 +47,13 @@ import {
GetModuleOnlyTreeUrl,
GetModuleTreeUrl,
GetPoolId,
GetPoolOptionUrl,
GetTrashModuleCountUrl,
GetTrashModuleTreeUrl,
ImportDefinitionUrl,
MockDetailUrl,
MoveModuleUrl,
OperationHistoryUrl,
PoolOption,
RecoverCaseUrl,
RecoverDefinitionUrl,
RecoverOperationHistoryUrl,
@ -519,11 +518,6 @@ export function batchExecuteCase(data: ApiCaseBatchExecuteParams) {
return MSR.post({ url: BatchExecuteCaseUrl, data });
}
// 获取接口测试-环境列表
export function getEnvList(projectId: string) {
return MSR.get<Environment[]>({ url: GetEnvListUrl, params: projectId });
}
// 获取接口用例-执行历史
export function getApiCaseExecuteHistory(data: ApiCaseExecuteHistoryParams) {
return MSR.post<CommonList<ApiCaseExecuteHistoryItem>>({ url: GetExecuteHistoryUrl, data });
@ -541,7 +535,7 @@ export function getApiCaseDependency(data: ApiCaseDependencyParams) {
// 获取接口的资源池列表
export function getPoolOption(projectId: string) {
return MSR.get<ResourcePoolItem[]>({ url: PoolOption + projectId });
return MSR.get<ResourcePoolItem[]>({ url: GetPoolOptionUrl, params: projectId });
}
export function getPoolId(projectId: string) {

View File

@ -29,6 +29,7 @@ import {
DisassociateApiScenarioUrl,
DisassociateCaseUrl,
dragPlanOnGroupUrl,
EditPlanMinderUrl,
ExecuteHistoryUrl,
ExecutePlanUrl,
followPlanUrl,
@ -103,6 +104,7 @@ import type {
PlanDetailExecuteHistoryItem,
PlanDetailFeatureCaseItem,
PlanDetailFeatureCaseListQueryParams,
PlanMinderEditParams,
PlanMinderNode,
RunFeatureCaseParams,
SortApiCaseParams,
@ -408,3 +410,7 @@ export function deleteScheduleTask(testPlanId: string) {
export function getPlanMinder(testPlanId: string) {
return MSR.get<PlanMinderNode[]>({ url: GetPlanMinderUrl, params: testPlanId });
}
// 更新测试规划脑图
export function editPlanMinder(data: PlanMinderEditParams) {
return MSR.post({ url: EditPlanMinderUrl, data });
}

View File

@ -77,7 +77,6 @@ export const TransferFileCaseUrl = '/api/case/transfer'; // 文件转存
export const TransferFileModuleOptionCaseUrl = '/api/case/transfer/options'; // 文件转存目录
export const UploadTempFileCaseUrl = '/api/case/upload/temp/file'; // 临时文件上传
export const GetCaseDetailUrl = '/api/case/get-detail'; // 获取接口用例详情
export const GetEnvListUrl = '/api/test/env-list'; // 接口测试-环境列表
export const BatchExecuteCaseUrl = '/api/case/batch/run'; // 批量执行接口用例
export const ExecuteCaseUrl = '/api/case/run'; // 单独执行接口用例
export const GetExecuteHistoryUrl = '/api/case/execute/page'; // 获取用的执行历史
@ -98,5 +97,5 @@ export const DeleteRecycleCaseUrl = '/api/case/delete'; // 接口用例彻底删
export const BatchDeleteRecycleCaseUrl = '/api/case/batch/delete'; // 接口用例批量彻底删除
export const AddCaseUrl = '/api/case/add'; // 添加用例
export const PoolOption = '/api/test/pool-option/'; // 获取接口资源池
export const GetPoolOptionUrl = '/api/test/pool-option'; // 获取接口资源池
export const GetPoolId = '/api/test/get-pool/'; // 获取项目应用设置的资源池id

View File

@ -143,3 +143,5 @@ export const BatchMoveApiScenarioUrl = '/test-plan/api/scenario/batch/move';
// 测试规划脑图
export const GetPlanMinderUrl = '/test-plan/mind/data';
// 修改测试规划脑图
export const EditPlanMinderUrl = '/test-plan/mind/data/edit';

View File

@ -25,6 +25,7 @@
@action="handleAction"
@before-exec-command="handleBeforeExecCommand"
@save="handleMinderSave"
@float-menu-close="handleBaseInfoCancel"
>
<template #extractMenu>
<a-tooltip v-if="showDetailMenu" :content="t('common.detail')">
@ -258,69 +259,6 @@
};
}
/**
* 生成脑图保存的入参
*/
function makeMinderParams(fullJson: MinderJson): FeatureCaseMinderUpdateParams {
filterTree(fullJson.root.children, (node, nodeIndex, parent) => {
if (node.data.isNew !== false || node.data.changed === true) {
if (node.data.resource?.includes(moduleTag)) {
//
tempMinderParams.value.updateModuleList.push({
id: node.data.id,
name: node.data.text,
parentId: parent?.data.id || 'NONE',
type: node.data.isNew !== false ? 'ADD' : 'UPDATE',
...getNodeMoveInfo(nodeIndex, parent as MinderJsonNode),
});
} else if (node.data.resource?.includes(caseTag)) {
//
const caseNodeInfo = getCaseNodeInfo(node as MinderJsonNode);
const caseBaseInfo = baseInfoRef.value?.makeParams();
tempMinderParams.value.updateCaseList.push({
id: node.data.id,
moduleId: parent?.data.id || '',
type: node.data.isNew !== false ? 'ADD' : 'UPDATE',
templateId: templateId.value,
tags: caseBaseInfo?.tags || [],
customFields: caseBaseInfo?.customFields || [],
name: caseBaseInfo?.name || node.data.text,
...getNodeMoveInfo(nodeIndex, parent as MinderJsonNode),
...caseNodeInfo,
});
return false; //
} else if (!node.data.resource || node.data.resource.length === 0) {
//
tempMinderParams.value.additionalNodeList.push({
id: node.data.id,
parentId: parent?.data.id || 'NONE',
type: node.data.isNew !== false ? 'ADD' : 'UPDATE',
name: node.data.text,
...getNodeMoveInfo(nodeIndex, parent as MinderJsonNode),
});
}
}
return true;
});
return tempMinderParams.value;
}
async function handleMinderSave(fullJson: MinderJson, callback: () => void) {
try {
loading.value = true;
await saveCaseMinder(makeMinderParams(fullJson));
Message.success(t('common.saveSuccess'));
initCaseTree();
callback();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
loading.value = false;
}
}
/**
* 已选中节点的可替换标签判断
* @param node 选中节点
@ -906,6 +844,31 @@
}
}
/**
* 切换用例详情显示
*/
async function toggleDetail(val?: boolean) {
extraVisible.value = val !== undefined ? val : !extraVisible.value;
const node: MinderJsonNode = window.minder.getSelectedNode();
const { data } = node;
if (extraVisible.value) {
if (data?.resource && data.resource.includes(caseTag)) {
activeExtraKey.value = 'baseInfo';
resetExtractInfo();
if (data.isNew === false) {
//
initCaseDetail(data);
} else {
activeCase.value = {
id: data.id,
name: data.text,
isNew: true,
};
}
}
}
}
const showDetailMenu = ref(false);
const canShowEnterNode = ref(false);
/**
@ -929,6 +892,9 @@
if (data?.resource && data.resource.includes(caseTag)) {
//
showDetailMenu.value = true;
if (extraVisible.value) {
toggleDetail(true);
}
} else if (data?.resource?.includes(moduleTag) && data.count > 0 && data.isLoaded !== true) {
//
try {
@ -1014,31 +980,6 @@
}
}
/**
* 切换用例详情显示
*/
async function toggleDetail() {
extraVisible.value = !extraVisible.value;
const node: MinderJsonNode = window.minder.getSelectedNode();
const { data } = node;
if (extraVisible.value) {
if (data?.resource && data.resource.includes(caseTag)) {
activeExtraKey.value = 'baseInfo';
resetExtractInfo();
if (data.isNew === false) {
//
initCaseDetail(data);
} else {
activeCase.value = {
id: data.id,
name: data.text,
isNew: true,
};
}
}
}
}
/**
* 标签编辑后如果将标签修改为模块则删除已添加的优先级
* @param node 选中节点
@ -1334,6 +1275,70 @@
});
}
}
/**
* 生成脑图保存的入参
*/
function makeMinderParams(fullJson: MinderJson): FeatureCaseMinderUpdateParams {
filterTree(fullJson.root.children, (node, nodeIndex, parent) => {
if (node.data.isNew !== false || node.data.changed === true) {
if (node.data.resource?.includes(moduleTag)) {
//
tempMinderParams.value.updateModuleList.push({
id: node.data.id,
name: node.data.text,
parentId: parent?.data.id || 'NONE',
type: node.data.isNew !== false ? 'ADD' : 'UPDATE',
...getNodeMoveInfo(nodeIndex, parent as MinderJsonNode),
});
} else if (node.data.resource?.includes(caseTag)) {
//
const caseNodeInfo = getCaseNodeInfo(node as MinderJsonNode);
const caseBaseInfo = baseInfoRef.value?.makeParams();
tempMinderParams.value.updateCaseList.push({
id: node.data.id,
moduleId: parent?.data.id || '',
type: node.data.isNew !== false ? 'ADD' : 'UPDATE',
templateId: templateId.value,
tags: caseBaseInfo?.tags || [],
customFields: caseBaseInfo?.customFields || [],
name: caseBaseInfo?.name || node.data.text,
...getNodeMoveInfo(nodeIndex, parent as MinderJsonNode),
...caseNodeInfo,
});
return false; //
} else if (!node.data.resource || node.data.resource.length === 0) {
//
tempMinderParams.value.additionalNodeList.push({
id: node.data.id,
parentId: parent?.data.id || 'NONE',
type: node.data.isNew !== false ? 'ADD' : 'UPDATE',
name: node.data.text,
...getNodeMoveInfo(nodeIndex, parent as MinderJsonNode),
});
}
}
return true;
});
return tempMinderParams.value;
}
async function handleMinderSave(fullJson: MinderJson, callback: () => void) {
try {
loading.value = true;
await saveCaseMinder(makeMinderParams(fullJson));
extraVisible.value = false;
Message.success(t('common.saveSuccess'));
initCaseTree();
callback();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
loading.value = false;
}
}
</script>
<style lang="less" scoped></style>

View File

@ -4,7 +4,7 @@
v-model:loading="loading"
v-model:import-json="importJson"
:tags="[]"
:insert-node="insertNode"
:insert-node="(node, type) => insertNode(node as PlanMinderNode,type)"
:can-show-enter-node="false"
:insert-sibling-menus="insertSiblingMenus"
:insert-son-menus="insertSonMenus"
@ -12,16 +12,22 @@
:can-show-more-menu="false"
:can-show-priority-menu="false"
:can-show-float-menu="canShowFloatMenu"
:can-show-delete-menu="canShowDeleteMenu"
custom-priority
single-tag
tag-enable
sequence-enable
@content-change="handleContentChange"
@node-select="checkNodeCanShowMenu"
@node-select="(node) => handleNodeSelect(node as PlanMinderNode)"
@before-exec-command="handleBeforeExecCommand"
@save="handleMinderSave"
@float-menu-close="handleFloatMenuClose"
>
<template #extractMenu>
<a-tooltip v-if="showAssociateCaseMenu" :content="t('ms.case.associate.title')">
<MsButton type="icon" class="ms-minder-node-float-menu-icon-button" @click="associateCase">
<MsIcon type="icon-icon_add_outlined" class="text-[var(--color-text-4)]" />
</MsButton>
</a-tooltip>
<a-dropdown
v-if="canShowExecuteMethodMenu"
v-model:popup-visible="executeMethodMenuVisible"
@ -65,14 +71,20 @@
type="icon"
class="ms-minder-node-float-menu-icon-button"
:class="[extraVisible ? 'ms-minder-node-float-menu-icon-button--focus' : '']"
@click="toggleDetail"
@click="toggleConfig"
>
<MsIcon type="icon-icon_setting_filled" class="text-[var(--color-text-4)]" />
</MsButton>
</a-tooltip>
</template>
<template #extractTabContent>
<div class="px-[16px]">
<div v-if="configForm" class="px-[16px]">
<div class="mb-[16px] flex items-center gap-[8px]">
<div class="h-[14px] w-[3px] rounded-[var(--border-radius-small)] bg-[rgb(var(--primary-4))]"></div>
<a-tooltip :content="configForm.text" position="tl">
<div class="one-line-text font-medium">{{ configForm.text }}</div>
</a-tooltip>
</div>
<a-form ref="configFormRef" :model="configForm" layout="vertical">
<a-form-item>
<template #label>
@ -113,76 +125,100 @@
</div>
</div>
</a-form-item>
<a-form-item :label="t('system.project.resourcePool')">
<a-select v-model:model-value="configForm.resourcePool" :options="resourcePoolOptions"></a-select>
</a-form-item>
<a-form-item class="hidden-item">
<a-radio-group v-model:model-value="configForm.executeType">
<a-radio value="serial">{{ t('testPlan.testPlanIndex.serial') }}</a-radio>
<a-radio value="parallel">{{ t('testPlan.testPlanIndex.parallel') }}</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item v-if="configForm.executeType === 'serial'" class="hidden-item">
<div class="flex items-center gap-[8px]">
<a-switch v-model:model-value="configForm.failStop" size="small"></a-switch>
<div>{{ t('ms.minders.failStop') }}</div>
</div>
</a-form-item>
<a-form-item class="hidden-item">
<div class="flex items-center gap-[8px]">
<a-switch v-model:model-value="configForm.failRetry" size="small"></a-switch>
<div>{{ t('ms.minders.failRetry') }}</div>
</div>
</a-form-item>
<template v-if="configForm.failRetry">
<template
v-if="
configForm.type !== PlanMinderCollectionType.FUNCTIONAL &&
(configForm.level === 1 || !configForm.extended)
"
>
<a-form-item :label="t('system.project.resourcePool')">
<a-select v-model:model-value="configForm.testResourcePoolId" :options="resourcePoolOptions"></a-select>
</a-form-item>
<a-form-item :label="t('project.environmental.env')">
<a-select v-model:model-value="configForm.environmentId" :options="environmentOptions"></a-select>
</a-form-item>
<a-form-item class="hidden-item">
<a-radio-group v-model:model-value="configForm.failRetryType">
<a-radio value="step">{{ t('ms.minders.stepRetry') }}</a-radio>
<a-radio value="scenario">{{ t('ms.minders.scenarioRetry') }}</a-radio>
<a-radio-group v-model:model-value="configForm.executeMethod">
<a-radio :value="RunMode.SERIAL">{{ t('testPlan.testPlanIndex.serial') }}</a-radio>
<a-radio :value="RunMode.PARALLEL">{{ t('testPlan.testPlanIndex.parallel') }}</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item>
<template #label>
<div class="flex items-center">
<div>{{ t('ms.minders.retry') }}</div>
<div class="text-[var(--color-text-4)]">{{ t('ms.minders.retryTimes') }}</div>
</div>
</template>
<a-input-number
v-model:model-value="configForm.retryTimes"
mode="button"
:step="1"
:min="1"
:precision="0"
size="small"
class="w-[120px]"
></a-input-number>
<a-form-item v-if="configForm.executeMethod === RunMode.SERIAL" class="hidden-item">
<div class="flex items-center gap-[8px]">
<a-switch v-model:model-value="configForm.stopOnFail" size="small"></a-switch>
<div>{{ t('ms.minders.failStop') }}</div>
</div>
</a-form-item>
<a-form-item>
<template #label>
<div class="flex items-center">
<div>{{ t('ms.minders.retrySpace') }}</div>
<div class="text-[var(--color-text-4)]">{{ t('ms.minders.retrySpaces') }}</div>
</div>
</template>
<a-input-number
v-model:model-value="configForm.retrySpace"
mode="button"
:step="100"
:min="0"
:precision="0"
size="small"
class="w-[120px]"
></a-input-number>
<a-form-item class="hidden-item">
<div class="flex items-center gap-[8px]">
<a-switch v-model:model-value="configForm.retryOnFail" size="small"></a-switch>
<div>{{ t('ms.minders.failRetry') }}</div>
</div>
</a-form-item>
<template v-if="configForm.retryOnFail">
<a-form-item v-if="configForm.type === PlanMinderCollectionType.SCENARIO" class="hidden-item">
<a-radio-group v-model:model-value="configForm.retryType">
<a-radio :value="FailRetry.STEP">{{ t('ms.minders.stepRetry') }}</a-radio>
<a-radio :value="FailRetry.SCENARIO">{{ t('ms.minders.scenarioRetry') }}</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item>
<template #label>
<div class="flex items-center">
<div>{{ t('ms.minders.retry') }}</div>
<div class="text-[var(--color-text-4)]">{{ t('ms.minders.retryTimes') }}</div>
</div>
</template>
<a-input-number
v-model:model-value="configForm.retryTimes"
mode="button"
:step="1"
:min="1"
:precision="0"
size="small"
class="w-[120px]"
></a-input-number>
</a-form-item>
<a-form-item>
<template #label>
<div class="flex items-center">
<div>{{ t('ms.minders.retrySpace') }}</div>
<div class="text-[var(--color-text-4)]">{{ t('ms.minders.retrySpaces') }}</div>
</div>
</template>
<a-input-number
v-model:model-value="configForm.retryInterval"
mode="button"
:step="100"
:min="0"
:precision="0"
size="small"
class="w-[120px]"
></a-input-number>
</a-form-item>
</template>
</template>
<a-form-item class="hidden-item">
<a-form-item
v-if="configForm.type !== PlanMinderCollectionType.FUNCTIONAL && configForm.level === 2"
class="hidden-item"
>
<div class="flex items-center gap-[8px]">
<a-switch v-model:model-value="configForm.extend" size="small"></a-switch>
<a-switch v-model:model-value="configForm.extended" size="small"></a-switch>
<div>{{ t('ms.minders.extend') }}</div>
</div>
</a-form-item>
</a-form>
<div class="flex items-center gap-[12px] bg-white pb-[16px]">
<a-button
v-permission="['FUNCTIONAL_CASE:READ+UPDATE']"
type="primary"
:loading="loading"
@click="handleConfigSave"
>
{{ t('common.save') }}
</a-button>
<a-button type="secondary" :disabled="loading" @click="handleConfigCancel">{{ t('common.cancel') }}</a-button>
</div>
</div>
</template>
</MsMinderEditor>
@ -190,29 +226,43 @@
v-model:visible="caseAssociateVisible"
v-model:currentSelectCase="currentSelectCase"
:has-not-associated-ids="selectedAssociateCasesParams.selectIds"
test-plan-id=""
:test-plan-id="props.planId"
@success="writeAssociateCases"
/>
</template>
<script setup lang="ts">
import { FormInstance, SelectOptionData } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsMinderEditor from '@/components/pure/ms-minder-editor/minderEditor.vue';
import { InsertMenuItem, MinderEvent, MinderJson, MinderJsonNode } from '@/components/pure/ms-minder-editor/props';
import {
InsertMenuItem,
MinderEvent,
MinderJson,
MinderJsonNode,
MinderJsonNodeData,
} from '@/components/pure/ms-minder-editor/props';
import { setCustomPriorityView } from '@/components/pure/ms-minder-editor/script/tool/utils';
import caseAssociate from './associateDrawer.vue';
import { getPlanMinder } from '@/api/modules/test-plan/testPlan';
import { getPoolOption } from '@/api/modules/api-test/management';
import { editPlanMinder, getPlanMinder } from '@/api/modules/test-plan/testPlan';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import useMinderStore from '@/store/modules/components/minder-editor';
import { filterTree, mapTree } from '@/utils';
import { filterTree, getGenerateId, mapTree } from '@/utils';
import { AssociateCaseRequest } from '@/models/testPlan/testPlan';
import {
AssociateCaseRequest,
PlanMinderEditListItem,
PlanMinderNode,
PlanMinderNodeData,
} from '@/models/testPlan/testPlan';
import { CaseLinkEnum } from '@/enums/caseEnum';
import { RunMode } from '@/enums/testPlanEnum';
import { MinderEventName } from '@/enums/minderEnum';
import { FailRetry, PlanMinderAssociateType, PlanMinderCollectionType, RunMode } from '@/enums/testPlanEnum';
import Message from '@arco-design/web-vue/es/message';
@ -224,50 +274,42 @@
const { t } = useI18n();
const minderStore = useMinderStore();
const loading = ref(false);
const extraVisible = ref<boolean>(false);
const importJson = ref<MinderJson>({
root: {} as MinderJsonNode,
template: 'default',
treePath: [],
});
const caseCountTag = t('ms.minders.caseCount');
const resourcePoolTag = t('ms.minders.resourcePool');
const envTag = t('ms.minders.env');
/**
* 插入节点
* @param node 目标节点
* @param type 插入类型
* @param value 插入值
*/
function insertNode(node: MinderJsonNode, type: string, value?: string) {
switch (type) {
case 'AppendChildNode':
break;
case 'AppendSiblingNode':
break;
default:
break;
}
}
function handleContentChange(node: MinderJsonNode) {}
const canShowFloatMenu = ref(false);
const insertSiblingMenus = ref<InsertMenuItem[]>([]);
const insertSonMenus = ref<InsertMenuItem[]>([]);
const canShowFloatMenu = ref(false);
const showAssociateCaseMenu = ref(false);
const canShowExecuteMethodMenu = ref(true);
const executeMethodMenuVisible = ref(false);
const showConfigMenu = ref(false);
const canShowDeleteMenu = ref(false);
const extraVisible = ref<boolean>(false);
/**
* 检测节点可展示的菜单项
* @param node 选中节点
*/
function checkNodeCanShowMenu(node: MinderJsonNode) {
function checkNodeCanShowMenu(node: PlanMinderNode) {
const { data } = node;
if (data?.level === 1 || data?.level === 2) {
canShowFloatMenu.value = true;
canShowExecuteMethodMenu.value = true;
if (data?.type === PlanMinderCollectionType.FUNCTIONAL) {
//
canShowExecuteMethodMenu.value = false;
} else {
canShowExecuteMethodMenu.value = true;
}
if (data?.level === 1) {
//
insertSiblingMenus.value = [];
insertSonMenus.value = [
{
@ -275,46 +317,300 @@
label: t('ms.minders.testSet'),
},
];
showConfigMenu.value = true;
if (data?.type === PlanMinderCollectionType.FUNCTIONAL) {
// 西
showConfigMenu.value = false;
} else {
showConfigMenu.value = true;
}
showAssociateCaseMenu.value = false;
canShowDeleteMenu.value = false;
} else {
insertSiblingMenus.value = [];
//
insertSiblingMenus.value = [
{
value: 'testSet',
label: t('ms.minders.testSet'),
},
];
insertSonMenus.value = [];
showAssociateCaseMenu.value = true;
showConfigMenu.value = true;
canShowDeleteMenu.value = true;
}
} else {
canShowFloatMenu.value = false;
canShowExecuteMethodMenu.value = false;
showAssociateCaseMenu.value = false;
showConfigMenu.value = false;
extraVisible.value = false;
canShowDeleteMenu.value = false;
}
}
/**
* 执行插入节点
* @param command 插入命令
* @param node 目标节点
*/
function execInert(command: string, data?: PlanMinderNodeData | MinderJsonNodeData) {
if (window.minder.queryCommandState(command) !== -1) {
window.minder.execCommand(command, data);
}
}
/**
* 插入节点
* @param node 目标节点
* @param type 插入类型
* @param value 插入值
*/
function insertNode(node: PlanMinderNode, type: string) {
let child: PlanMinderNodeData | undefined;
//
const caseCountNodeData = {
id: getGenerateId(),
text: t('ms.minders.item', { count: 0 }),
resource: [caseCountTag],
level: 3,
isNew: true,
};
//
const envNodeData = {
id: getGenerateId(),
text: t('case.execute.defaultEnv'),
resource: [envTag],
level: 3,
isNew: true,
};
//
const resourcePoolNodeData = {
id: getGenerateId(),
resource: [resourcePoolTag],
text: t('ms.minders.defaultResourcePool'),
level: 3,
isNew: true,
};
if (node.data?.level === 1) {
//
child = {
...node.data,
id: getGenerateId(),
text: t('ms.minders.defaultTestSet'),
level: 2,
isNew: true,
};
} else if (node.parent?.data) {
//
child = {
...(node.parent.data as PlanMinderNodeData),
id: getGenerateId(),
text: t('ms.minders.defaultTestSet'),
level: 2,
isNew: true,
};
}
if (child) {
execInert(type, child);
nextTick(() => {
execInert('AppendChildNode', caseCountNodeData);
if (node.data?.type !== PlanMinderCollectionType.FUNCTIONAL) {
//
execInert('AppendSiblingNode', envNodeData);
execInert('AppendSiblingNode', resourcePoolNodeData);
}
});
}
}
//
const activePlanSet = ref<PlanMinderNode>();
const currentPriority = ref<RunMode>(RunMode.SERIAL);
// /
const priorityTextMap = {
2: t('ms.minders.serial'),
3: t('ms.minders.parallel'),
};
// /
const priorityMap = {
[RunMode.SERIAL]: 2,
[RunMode.PARALLEL]: 3,
};
/**
* 处理执行方式切换
* @param val 执行方式
*/
function handleExecuteMethodMenuSelect(val: RunMode) {
currentPriority.value = val;
//
window.minder.execCommand('priority', priorityMap[val]);
// DOM
setCustomPriorityView(priorityTextMap);
}
const configFormRef = ref<FormInstance>();
const configForm = ref<PlanMinderNodeData>();
const resourcePoolOptions = ref<SelectOptionData[]>();
const environmentOptions = computed(() => [
{
label: t('testPlan.testPlanIndex.defaultEnv'),
value: 'NONE',
},
...appStore.getEnvList.map((item) => ({ label: item.name, value: item.id })),
]);
// configForm
const switchingConfigFormData = ref(false);
const configFormUnsaved = ref(false);
function handleConfigCancel() {
extraVisible.value = false;
activePlanSet.value = undefined;
configForm.value = undefined;
configFormUnsaved.value = false;
}
/**
* 切换用例详情显示
* 检查配置表单是否未保存
*/
async function toggleDetail() {
extraVisible.value = !extraVisible.value;
function checkConfigFormUnsaved() {
const node: MinderJsonNode = window.minder.getSelectedNode();
const { data } = node;
if (extraVisible.value) {
if (data?.resource && data.resource.includes('')) {
console.log();
if (configFormUnsaved.value) {
if (node?.data?.id !== configForm.value?.id) {
//
Message.warning(t('ms.minders.unsavedTip'));
//
if (node) {
window.minder.toggleSelect(node);
}
window.minder.selectById(configForm.value?.id);
}
return true;
}
return false;
}
function handleFloatMenuClose() {
if (!checkConfigFormUnsaved()) {
handleConfigCancel();
}
}
/**
* 处理节点选中
* @param node 节点
*/
function handleNodeSelect(node: PlanMinderNode) {
if (checkConfigFormUnsaved()) {
return;
}
checkNodeCanShowMenu(node);
if (extraVisible.value) {
if (node.data?.type === PlanMinderCollectionType.FUNCTIONAL && node.data?.level === 1) {
//
extraVisible.value = false;
return;
}
activePlanSet.value = node;
switchingConfigFormData.value = true;
configForm.value = cloneDeep(activePlanSet.value.data);
nextTick(() => {
switchingConfigFormData.value = false;
});
}
}
/**
* 切换测试集配置显示
*/
function toggleConfig() {
if (checkConfigFormUnsaved()) {
//
Message.warning(t('ms.minders.unsavedTip'));
return;
}
extraVisible.value = !extraVisible.value;
const node: MinderJsonNode = window.minder.getSelectedNode();
switchingConfigFormData.value = true;
if (extraVisible.value) {
activePlanSet.value = node as PlanMinderNode;
configForm.value = cloneDeep(activePlanSet.value.data);
} else {
activePlanSet.value = undefined;
configForm.value = undefined;
}
nextTick(() => {
switchingConfigFormData.value = false;
});
}
const currentSelectCase = ref<keyof typeof CaseLinkEnum>('FUNCTIONAL');
const caseAssociateVisible = ref<boolean>(false);
//
const selectedAssociateCasesParams = ref<AssociateCaseRequest>({
excludeIds: [],
selectIds: [],
selectAll: false,
condition: {},
moduleIds: [],
versionId: '',
refId: '',
projectId: '',
});
function writeAssociateCases(param: AssociateCaseRequest) {
selectedAssociateCasesParams.value = { ...param };
const node: PlanMinderNode = window.minder.getSelectedNode();
node.data.associateDTOS = [
{
ids: param.selectIds,
associateType:
node.data.type === PlanMinderCollectionType.SCENARIO ? PlanMinderAssociateType.SCENARIO_CASE : node.data.type,
},
];
caseAssociateVisible.value = false;
}
function clearSelectedCases() {
selectedAssociateCasesParams.value = {
excludeIds: [],
selectIds: [],
selectAll: false,
condition: {},
moduleIds: [],
versionId: '',
refId: '',
projectId: '',
};
}
function associateCase() {
const node: PlanMinderNode = window.minder.getSelectedNode();
activePlanSet.value = node;
switchingConfigFormData.value = true;
configForm.value = cloneDeep(activePlanSet.value.data);
extraVisible.value = true;
currentSelectCase.value = node.data?.type || 'FUNCTIONAL';
caseAssociateVisible.value = true;
nextTick(() => {
switchingConfigFormData.value = false;
});
}
watch(
() => [configForm.value, selectedAssociateCasesParams.value.selectIds],
() => {
if (!switchingConfigFormData.value && configForm.value) {
configFormUnsaved.value = true;
}
},
{
deep: true,
}
);
/**
* 是否停止拖拽排序动作
* @param dragNode 拖动节点
@ -326,8 +622,16 @@
}
for (let i = 0; i < dragNodes.length; i++) {
const dragNode = (dragNodes as MinderJsonNode[])[i];
if (dragNode.parent?.data?.id !== dropNode.parent?.data?.id) {
//
return true;
}
if (dragNode.data?.level === 2) {
//
return false;
}
}
return true;
return false;
}
/**
@ -358,62 +662,6 @@
}
}
const tempMinderParams = ref({
projectId: appStore.currentProjectId,
versionId: '',
updateCaseList: [],
updateModuleList: [],
deleteResourceList: [],
additionalNodeList: [],
});
const configFormRef = ref<FormInstance>();
const configForm = ref({
resourcePool: '',
executeType: 'serial',
failStop: true,
failRetry: true,
failRetryType: 'step',
retryTimes: 1,
retrySpace: 1000,
extend: true,
});
const resourcePoolOptions = ref<SelectOptionData[]>();
const currentSelectCase = ref<keyof typeof CaseLinkEnum>('FUNCTIONAL');
const caseAssociateVisible = ref<boolean>(false);
const caseAssociateProject = ref(appStore.currentProjectId);
//
const selectedAssociateCasesParams = ref<AssociateCaseRequest>({
excludeIds: [],
selectIds: [],
selectAll: false,
condition: {},
moduleIds: [],
versionId: '',
refId: '',
projectId: '',
});
function writeAssociateCases(param: AssociateCaseRequest) {
selectedAssociateCasesParams.value = { ...param };
caseAssociateVisible.value = false;
}
function clearSelectedCases() {
selectedAssociateCasesParams.value = {
excludeIds: [],
selectIds: [],
selectAll: false,
condition: {},
moduleIds: [],
versionId: '',
refId: '',
projectId: '',
};
}
/**
* 初始化测试规划脑图
*/
@ -427,6 +675,7 @@
level,
isNew: false,
changed: false,
disabled: level < 2,
};
return node;
});
@ -440,15 +689,30 @@
}
}
const tempMinderParams = ref({
planId: props.planId,
editList: [] as PlanMinderEditListItem[],
deletedIds: [],
});
/**
* 生成脑图保存的入参
*/
function makeMinderParams(fullJson: MinderJson) {
filterTree(fullJson.root.children, (node, nodeIndex, parent) => {
if (node.data.isNew !== false || node.data.changed === true) {
return true;
filterTree(fullJson.root.children, (node, nodeIndex) => {
if (node.data.isNew) {
tempMinderParams.value.editList.push({
...node.data,
id: undefined,
num: nodeIndex,
});
} else {
tempMinderParams.value.editList.push({
...node.data,
num: nodeIndex,
});
}
return true;
return node.data.level < 2;
});
return tempMinderParams.value;
}
@ -456,8 +720,9 @@
async function handleMinderSave(fullJson: MinderJson, callback: () => void) {
try {
loading.value = true;
// await saveCaseMinder(makeMinderParams(fullJson));
await editPlanMinder(makeMinderParams(fullJson));
Message.success(t('common.saveSuccess'));
handleConfigCancel();
initMinder();
callback();
} catch (error) {
@ -468,6 +733,40 @@
}
}
/**
* 保存测试集配置
*/
function handleConfigSave() {
configFormRef.value?.validate((errors) => {
if (!errors) {
const node: MinderJsonNode = window.minder.getSelectedNode();
if (node.data && configForm.value) {
node.data = {
...node.data,
...cloneDeep(configForm.value),
};
}
// SAVE_MINDER
minderStore.dispatchEvent(MinderEventName.SAVE_MINDER);
}
});
}
async function initResourcePoolList() {
try {
const res = await getPoolOption(appStore.currentProjectId);
resourcePoolOptions.value = res.map((e) => ({ label: e.name, value: e.id }));
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
onBeforeMount(() => {
initResourcePoolList();
appStore.initEnvList();
});
onMounted(() => {
initMinder();
nextTick(() => {
@ -478,6 +777,7 @@
}, 0);
});
window.minder.on('selectionchange', () => {
//
setTimeout(() => {
setCustomPriorityView(priorityTextMap);
}, 0);

View File

@ -12,4 +12,11 @@ export default {
'ms.minders.executeMethod': '运行方式',
'ms.minders.serial': '串',
'ms.minders.parallel': '并',
'ms.minders.defaultTestSet': '默认测试集',
'ms.minders.caseCount': '用例数',
'ms.minders.resourcePool': '资源池',
'ms.minders.defaultResourcePool': '默认资源池',
'ms.minders.env': '环境',
'ms.minders.item': '{count}条',
'ms.minders.unsavedTip': '请先保存您更改的配置!',
};

View File

@ -11,6 +11,7 @@ export interface UseEventListenerProps {
handleMinderEvent?: (event: MinderCustomEvent) => void;
handleBeforeExecCommand?: (event: MinderEvent) => void;
handleViewChange?: (event: MinderEvent) => void;
handleDragFinish?: (event: MinderEvent) => void;
}
export default function useEventListener(listener: UseEventListenerProps) {
@ -43,6 +44,14 @@ export default function useEventListener(listener: UseEventListenerProps) {
}
});
// 监听脑图节点拖拽结束事件
minder.on('dragFinish', (e: MinderEvent) => {
if (listener.handleDragFinish) {
listener.handleDragFinish(e);
}
});
// 监听脑图画布位移等视图变化
minder.on(
'viewchange',
debounce((e: MinderEvent) => {

View File

@ -42,6 +42,7 @@
const emit = defineEmits<{
(e: 'click', eventTag: string): void;
(e: 'save'): void;
(e: 'toggleFullScreen', isFullScreen: boolean): void;
}>();
const { t } = useI18n();
@ -49,6 +50,13 @@
const containerRef = ref<Element | null>(null);
const { toggleFullScreen, isFullScreen } = useFullScreen(containerRef);
watch(
() => isFullScreen.value,
(value) => {
emit('toggleFullScreen', value);
}
);
onMounted(() => {
containerRef.value = document.querySelector('.ms-minder-editor-container');
});

View File

@ -12,7 +12,7 @@
</a-breadcrumb-item>
</a-breadcrumb>
</div>
<nodeFloatMenu v-if="props.canShowFloatMenu" v-bind="props">
<nodeFloatMenu v-if="props.canShowFloatMenu" v-bind="props" @close="emit('floatMenuClose')">
<template #extractMenu>
<slot name="extractMenu"></slot>
</template>
@ -33,6 +33,7 @@
import { MinderEventName } from '@/enums/minderEnum';
import useEventListener from '../hooks/useMinderEventListener';
import {
editMenuProps,
floatMenuProps,
@ -61,6 +62,7 @@
const emit = defineEmits<{
(e: 'save', data: MinderJson, callback: () => void): void;
(e: 'afterMount'): void;
(e: 'floatMenuClose'): void;
}>();
const minderStore = useMinderStore();
@ -79,43 +81,6 @@
treePath: [],
});
function handlePriorityButton() {
const { priorityPrefix } = props;
const { priorityStartWithZero } = props;
let start = priorityStartWithZero ? 0 : 1;
let res = '';
for (let i = 0; i < props.priorityCount; i++) {
res += start++;
}
const priority = window.minder.hotbox.state('priority');
res.replace(/./g, (p) => {
priority.button({
position: 'ring',
label: priorityPrefix + p,
key: p,
action() {
const pVal = parseInt(p, 10);
window.minder.execCommand('Priority', priorityStartWithZero ? pVal + 1 : pVal);
},
});
//
return '';
});
}
function handleTagButton() {
const tag = window.minder.hotbox.state('tag');
props.tags?.forEach((item) => {
tag.button({
position: 'ring',
label: item,
key: item,
action() {
window.minder.execCommand('resource', item);
},
});
});
}
async function init() {
window.editor = new Editor(mec.value, {
sequenceEnable: props.sequenceEnable,
@ -137,43 +102,42 @@
window.minder.execCommand('RemoveNode');
};
window.minder.on('preExecCommand', (env: any) => {
const selectNodes = env.minder.getSelectedNodes();
const notChangeCommands = new Set([
'camera',
'copy',
'expand',
'expandToLevel',
'hand',
'layout',
'template',
'theme',
'zoom',
'zoomIn',
'zoomOut',
]);
if (selectNodes && !notChangeCommands.has(env.commandName.toLocaleLowerCase())) {
minderStore.setMinderUnsaved(true);
minderStore.dispatchEvent(MinderEventName.MINDER_CHANGED);
selectNodes.forEach((node: MinderJsonNode) => {
markChangeNode(node);
});
}
if (env.commandName === 'movetoparent') {
setTimeout(() => {
const targetNode = window.minder.getSelectedNode();
targetNode.parent.renderTree();
}, 100);
}
});
handlePriorityButton();
handleTagButton();
emit('afterMount');
}
onMounted(async () => {
onMounted(() => {
init();
useEventListener({
handleBeforeExecCommand(event) {
const selectNodes: MinderJsonNode[] = event.minder.getSelectedNodes();
const notChangeCommands = new Set([
'camera',
'copy',
'expand',
'expandToLevel',
'hand',
'layout',
'template',
'theme',
'zoom',
'zoomIn',
'zoomOut',
]);
if (selectNodes.length > 0 && !notChangeCommands.has(event.commandName.toLocaleLowerCase())) {
minderStore.setMinderUnsaved(true);
minderStore.dispatchEvent(MinderEventName.MINDER_CHANGED);
selectNodes.forEach((node: MinderJsonNode) => {
markChangeNode(node);
});
}
if (event.commandName === 'movetoparent') {
setTimeout(() => {
const targetNode = window.minder.getSelectedNode();
targetNode.parent.renderTree();
}, 100);
}
},
});
});
const menuVisible = ref(false);
@ -206,23 +170,6 @@
}, 100); // TODO:
}
watch(
() => minderStore.event.timestamp,
() => {
if (minderStore.event.name === MinderEventName.HOTBOX && minderStore.event.nodePosition) {
const nodeDomWidth = minderStore.event.nodeDom?.getBoundingClientRect().width || 0;
menuPopupOffset.value = [
minderStore.event.nodePosition.x + nodeDomWidth / 2,
minderStore.event.nodePosition.y - nodeDomWidth / 4,
];
menuVisible.value = true;
}
if (minderStore.event.name === MinderEventName.ENTER_NODE && minderStore.event.nodes) {
switchNode(minderStore.event.nodes[0]);
}
}
);
function save() {
let data = importJson.value;
if (innerImportJson.value.treePath?.length > 1) {
@ -242,6 +189,26 @@
});
}
watch(
() => minderStore.event.timestamp,
() => {
if (minderStore.event.name === MinderEventName.HOTBOX && minderStore.event.nodePosition) {
const nodeDomWidth = minderStore.event.nodeDom?.getBoundingClientRect().width || 0;
menuPopupOffset.value = [
minderStore.event.nodePosition.x + nodeDomWidth / 2,
minderStore.event.nodePosition.y - nodeDomWidth / 4,
];
menuVisible.value = true;
}
if (minderStore.event.name === MinderEventName.ENTER_NODE && minderStore.event.nodes) {
switchNode(minderStore.event.nodes[0]);
}
if (minderStore.event.name === MinderEventName.SAVE_MINDER) {
save();
}
}
);
watch(
() => minderStore.getMinderUnsaved,
(val) => {
@ -256,10 +223,9 @@
@apply !absolute;
}
.ms-minder-container {
@apply relative overflow-hidden !bg-white;
@apply relative h-full overflow-hidden !bg-white;
padding: 16px 0;
height: calc(100% - 60px);
}
.ms-minder-dropdown {
.arco-dropdown-list-wrapper {

View File

@ -166,6 +166,11 @@
</a-doption>
</template>
</a-dropdown>
<a-tooltip v-else-if="props.canShowDeleteMenu" :content="t('common.delete')">
<MsButton type="icon" class="ms-minder-node-float-menu-icon-button" @click="handleMinderMenuSelect('delete')">
<MsIcon type="icon-icon_delete-trash_outlined" class="text-[var(--color-text-4)]" />
</MsButton>
</a-tooltip>
</template>
</a-trigger>
</template>
@ -179,7 +184,7 @@
import { useI18n } from '@/hooks/useI18n';
import useMinderStore from '@/store/modules/components/minder-editor/index';
import { MinderNodePosition } from '@/store/modules/components/minder-editor/types';
import { getGenerateId } from '@/utils';
import { getGenerateId, sleep } from '@/utils';
import { MinderEventName } from '@/enums/minderEnum';
@ -192,6 +197,9 @@
...tagProps,
...priorityProps,
});
const emit = defineEmits<{
(e: 'close'): void;
}>();
const { t } = useI18n();
const minderStore = useMinderStore();
@ -204,34 +212,37 @@
watch(
() => minderStore.event.timestamp,
() => {
let nodePosition: MinderNodePosition | undefined;
const selectedNodes: MinderJsonNode[] = window.minder.getSelectedNodes();
if (minderStore.event.name === MinderEventName.NODE_SELECT) {
nodePosition = minderStore.event.nodePosition;
currentNodeTags.value = minderStore.event.nodes?.[0].data?.resource || [];
if (props.replaceableTags) {
tags.value = props.replaceableTags(selectedNodes);
} else {
tags.value = [];
async () => {
if (window.minder) {
let nodePosition: MinderNodePosition | undefined;
const selectedNodes: MinderJsonNode[] = window.minder.getSelectedNodes();
if (minderStore.event.name === MinderEventName.NODE_SELECT) {
nodePosition = minderStore.event.nodePosition;
currentNodeTags.value = minderStore.event.nodes?.[0].data?.resource || [];
if (props.replaceableTags) {
tags.value = props.replaceableTags(selectedNodes);
} else {
tags.value = [];
}
}
if (selectedNodes.length > 1) {
// TODO:
menuVisible.value = false;
return;
}
if ([MinderEventName.VIEW_CHANGE, MinderEventName.DRAG_FINISH].includes(minderStore.event.name)) {
//
await sleep(300); // 300ms
nodePosition = window.minder.getSelectedNode()?.getRenderBox();
}
if (nodePosition && isNodeInMinderView(undefined, nodePosition, nodePosition.width / 2)) {
//
const nodeDomHeight = nodePosition.height || 0;
menuPopupOffset.value = [nodePosition.x, nodePosition.y + nodeDomHeight + 4]; // 4px
menuVisible.value = true;
} else {
menuVisible.value = false;
}
}
if (selectedNodes.length > 1) {
// TODO:
menuVisible.value = false;
return;
}
if (minderStore.event.name === MinderEventName.VIEW_CHANGE) {
//
nodePosition = window.minder.getSelectedNode()?.getRenderBox();
}
if (nodePosition && isNodeInMinderView(undefined, nodePosition, nodePosition.width / 2)) {
//
const nodeDomHeight = nodePosition.height || 0;
menuPopupOffset.value = [nodePosition.x, nodePosition.y + nodeDomHeight + 4]; // 4px
menuVisible.value = true;
} else {
menuVisible.value = false;
}
},
{
@ -376,6 +387,15 @@
});
}
watch(
() => menuVisible.value,
(val) => {
if (!val) {
emit('close');
}
}
);
onMounted(() => {
nextTick(() => {
const freshFuc = setPriorityView;

View File

@ -1,7 +1,13 @@
<template>
<a-spin :loading="loading" class="ms-minder-editor-container">
<div class="flex-1">
<mainEditor v-model:import-json="importJson" v-bind="props" @after-mount="() => emit('afterMount')" @save="save">
<div class="relative flex-1 overflow-hidden">
<mainEditor
v-model:import-json="importJson"
v-bind="props"
@after-mount="() => emit('afterMount')"
@save="save"
@float-menu-close="emit('floatMenuClose')"
>
<template #extractMenu>
<slot name="extractMenu"></slot>
</template>
@ -52,6 +58,7 @@
(e: 'action', event: MinderCustomEvent): void;
(e: 'beforeExecCommand', event: MinderEvent): void;
(e: 'nodeUnselect'): void;
(e: 'floatMenuClose'): void;
}>();
const props = defineProps({
@ -126,6 +133,9 @@
handleViewChange() {
minderStore.dispatchEvent(MinderEventName.VIEW_CHANGE);
},
handleDragFinish() {
minderStore.dispatchEvent(MinderEventName.DRAG_FINISH);
},
});
});
</script>
@ -134,7 +144,7 @@
.ms-minder-editor-container {
@apply relative flex h-full w-full;
.ms-minder-editor-extra {
@apply flex flex-col overflow-hidden;
@apply flex flex-col overflow-hidden bg-white;
width: 0;
transition: all 300ms ease-in-out;

View File

@ -149,6 +149,11 @@ export const floatMenuProps = {
type: Boolean,
default: true,
},
// 不显示更多菜单时,是否显示删除菜单
canShowDeleteMenu: {
type: Boolean,
default: false,
},
// 是否显示进入节点
canShowEnterNode: {
type: Boolean,

View File

@ -13,6 +13,8 @@ export enum MinderEventName {
'NODE_UNSELECT' = 'NODE_UNSELECT', // 取消选中节点
'VIEW_CHANGE' = 'VIEW_CHANGE', // 脑图视图移动
'MINDER_CHANGED' = 'MINDER_CHANGED', // 脑图更改事件
'SAVE_MINDER' = 'SAVE_MINDER', // 脑图保存事件
'DRAG_FINISH' = 'DRAG_FINISH', // 脑图节点拖拽结束事件
}
export default {};

View File

@ -9,13 +9,21 @@ export enum RunMode {
PARALLEL = 'PARALLEL', // 并行
}
export enum TestSetType {
FUNCTIONAL_CASE = 'FUNCTIONAL_CASE',
API_CASE = 'API_CASE',
SCENARIO_CASE = 'SCENARIO_CASE',
}
export enum FailRetry {
STEP = 'STEP',
SCENARIO = 'SCENARIO',
}
// 功能FUNCTIONAL_CASE/接口定义API/接口用例API_CASE/场景SCENARIO_CASE
export enum PlanMinderAssociateType {
FUNCTIONAL_CASE = 'FUNCTIONAL',
API = 'API',
API_CASE = 'API_CASE',
SCENARIO_CASE = 'SCENARIO_CASE',
}
// 测试集类型(功能FUNCTIONAL/接口用例API/场景SCENARIO)
export enum PlanMinderCollectionType {
FUNCTIONAL = 'FUNCTIONAL',
API = 'API',
SCENARIO = 'SCENARIO',
}

View File

@ -1,11 +1,17 @@
import type { MinderJsonNodeData } from '@/components/pure/ms-minder-editor/props';
import type { MinderJsonNode, MinderJsonNodeData } from '@/components/pure/ms-minder-editor/props';
import type { BatchActionQueryParams } from '@/components/pure/ms-table/type';
import type { customFieldsItem } from '@/models/caseManagement/featureCase';
import type { TableQueryParams } from '@/models/common';
import { BatchApiParams, DragSortParams } from '@/models/common';
import { LastExecuteResults } from '@/enums/caseEnum';
import { type FailRetry, type RunMode, testPlanTypeEnum, type TestSetType } from '@/enums/testPlanEnum';
import {
type FailRetry,
type PlanMinderAssociateType,
type PlanMinderCollectionType,
type RunMode,
testPlanTypeEnum,
} from '@/enums/testPlanEnum';
export type planStatusType = 'PREPARED' | 'UNDERWAY' | 'COMPLETED' | 'ARCHIVED';
@ -375,9 +381,9 @@ export interface PlanMinderNodeData extends MinderJsonNodeData {
pos: number;
text: string;
num: number; // 关联用例数量
priority: string; // 串行/并行
executeMethod: RunMode; // 串行/并行值
type: TestSetType; // 测试集类型(功能/接口/场景)
priority?: number; // 串行/并行
executeMethod?: RunMode; // 串行/并行值
type: PlanMinderCollectionType; // 测试集类型(功能/接口/场景)
extended: boolean;
grouped: boolean; // 是否使用环境组
environmentId: string;
@ -388,7 +394,23 @@ export interface PlanMinderNodeData extends MinderJsonNodeData {
retryInterval: number;
stopOnFail: boolean;
}
export interface PlanMinderNode {
export interface PlanMinderNode extends MinderJsonNode {
data: PlanMinderNodeData;
children: PlanMinderNode[];
}
export interface PlanMinderAssociateDTO {
ids: string[];
associateType: PlanMinderAssociateType; // 关联关系的type(功能FUNCTIONAL_CASE/接口定义API/接口用例API_CASE/场景SCENARIO_CASE)
}
export interface PlanMinderEditListItem extends PlanMinderNodeData {
name: string;
collectionType: PlanMinderCollectionType; // 测试集类型(功能FUNCTIONAL_CASE/接口用例API_CASE/场景SCENARIO_CASE)
associateDTOS: PlanMinderAssociateDTO[];
}
export interface PlanMinderEditParams {
planId: string;
editList: PlanMinderEditListItem[];
deletedIds: string[];
}

View File

@ -16,6 +16,7 @@ import { useI18n } from '@/hooks/useI18n';
import { NO_PROJECT_ROUTE_NAME } from '@/router/constants';
import { watchStyle, watchTheme } from '@/utils/theme';
import type { EnvironmentItem } from '@/models/projectManagement/environmental';
import type { PageConfig, PageConfigKeys, Style, Theme } from '@/models/setting/config';
import { ProjectListItem } from '@/models/setting/project';
@ -108,6 +109,9 @@ const useAppStore = defineStore('app', {
getCurrentEnvId(state: AppState): string {
return state.currentEnvConfig?.id || '';
},
getEnvList(state: AppState): EnvironmentItem[] {
return state.envList;
},
},
actions: {
/**

View File

@ -105,7 +105,8 @@
import { BatchActionQueryParams } from '@/components/pure/ms-table/type';
import { getEnvList, getPoolId, getPoolOption } from '@/api/modules/api-test/management';
import { getEnvList } from '@/api/modules/api-test/common';
import { getPoolId, getPoolOption } from '@/api/modules/api-test/management';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';

View File

@ -105,7 +105,8 @@
import { BatchActionQueryParams } from '@/components/pure/ms-table/type';
import { getEnvList, getPoolId, getPoolOption } from '@/api/modules/api-test/management';
import { getEnvList } from '@/api/modules/api-test/common';
import { getPoolId, getPoolOption } from '@/api/modules/api-test/management';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';

View File

@ -506,7 +506,8 @@
import BatchRunModal from '@/views/api-test/scenario/components/batchRunModal.vue';
import operationScenarioModuleTree from '@/views/api-test/scenario/components/operationScenarioModuleTree.vue';
import { getEnvList, getPoolId, getPoolOption } from '@/api/modules/api-test/management';
import { getEnvList } from '@/api/modules/api-test/common';
import { getPoolId, getPoolOption } from '@/api/modules/api-test/management';
import {
batchEditScenario,
batchOptionScenario,

View File

@ -95,6 +95,7 @@
v-model:active-key="activeTab"
:get-text-func="getTabBadge"
:content-tab-list="tabList"
:change-interceptor="changeTabInterceptor"
no-content
class="relative mx-[16px] border-b"
/>
@ -424,6 +425,29 @@
return '';
}
}
function changeTabInterceptor(newVal: string, oldVal: string, done: () => void) {
console.log('changeTabInterceptor', newVal, oldVal);
if (oldVal === 'plan') {
openModal({
type: 'warning',
title: t('common.tip'),
content: t('ms.minders.leaveUnsavedTip'),
okText: t('common.confirm'),
cancelText: t('common.cancel'),
okButtonProps: {
status: 'normal',
},
onBeforeOk: async () => {
done();
},
hideCancel: false,
});
return;
}
done();
}
function handleSuccess() {
initDetail();
loadActiveTabList();