feat(脑图): 测试规划脑图雏形&测试计划详情去除关联用例

This commit is contained in:
baiqi 2024-06-11 19:51:05 +08:00 committed by Craftsman
parent 6b6f75ab2f
commit 03ff3517b1
31 changed files with 715 additions and 183 deletions

View File

@ -33,6 +33,7 @@ import {
GetFeatureCaseModuleUrl,
GetPlanDetailApiCaseListUrl,
GetPlanDetailFeatureCaseListUrl,
GetPlanMinderUrl,
getStatisticalCountUrl,
GetTestPlanCaseListUrl,
GetTestPlanDetailUrl,
@ -86,6 +87,7 @@ import type {
PlanDetailExecuteHistoryItem,
PlanDetailFeatureCaseItem,
PlanDetailFeatureCaseListQueryParams,
PlanMinderNode,
RunFeatureCaseParams,
SortApiCaseParams,
SortFeatureCaseParams,
@ -338,3 +340,7 @@ export function executePlanOrGroup(data: ExecutePlan) {
export function deleteScheduleTask(testPlanId: string) {
return MSR.get({ url: `${DeleteScheduleTaskUrl}/${testPlanId}` });
}
// 获取测试规划脑图
export function getPlanMinder(testPlanId: string) {
return MSR.get<PlanMinderNode[]>({ url: GetPlanMinderUrl, params: testPlanId });
}

View File

@ -110,3 +110,5 @@ export const DisassociateApiCaseUrl = '/test-plan/api/case/disassociate';
export const BatchDisassociateApiCaseUrl = '/test-plan/api/case/batch/disassociate';
// 计划详情-接口用例列表-批量更新执行人
export const BatchUpdateApiCaseExecutorUrl = '/test-plan/api/case/batch/update/executor';
// 测试规划脑图
export const GetPlanMinderUrl = '/test-plan/mind/data';

View File

@ -215,7 +215,7 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useVModel } from '@vueuse/core';
import { FormInstance, Message, SelectOptionData, ValidatedError } from '@arco-design/web-vue';
import { FormInstance, SelectOptionData, ValidatedError } from '@arco-design/web-vue';
import { MsAdvanceFilter } from '@/components/pure/ms-advance-filter';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
@ -226,7 +226,7 @@
import CaseTree from './caseTree.vue';
import ScenarioCaseTable from './scenarioCaseTable.vue';
import { getAssociatedProjectOptions, getCustomFieldsTable } from '@/api/modules/case-management/featureCase';
import { getAssociatedProjectOptions } from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';

View File

@ -212,7 +212,6 @@
import { CustomTypeMaps, MsAdvanceFilter } from '@/components/pure/ms-advance-filter';
import { FilterFormItem, FilterType } from '@/components/pure/ms-advance-filter/type';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';

View File

@ -1,16 +0,0 @@
<template>
<FeatureCaseMinder :module-id="props.moduleId" :module-name="props.moduleName" :modules-count="props.modulesCount" />
</template>
<script setup lang="ts">
import FeatureCaseMinder from './featureCaseMinder/index.vue';
const props = defineProps<{
minderType: 'FeatureCase';
moduleId: string;
moduleName: string;
modulesCount: Record<string, number>; //
}>();
</script>
<style lang="less" scoped></style>

View File

@ -26,7 +26,6 @@
import type { AssociateCaseRequest, AssociateCaseRequestType } from '@/models/testPlan/testPlan';
import { CaseCountApiTypeEnum, CaseModulesApiTypeEnum, CasePageApiTypeEnum } from '@/enums/associateCaseEnum';
import { CaseLinkEnum } from '@/enums/caseEnum';
const { t } = useI18n();
const props = defineProps<{
@ -49,29 +48,24 @@
const planId = ref(route.query.id as string);
async function saveHandler(params: AssociateCaseRequest) {
try {
confirmLoading.value = true;
if (typeof props.saveApi !== 'function') {
if (typeof props.saveApi !== 'function') {
emit('success', { ...params, functionalSelectIds: params.selectIds });
} else {
try {
confirmLoading.value = true;
await props.saveApi({
functionalSelectIds: params.selectIds,
testPlanId: planId.value,
});
emit('success', { ...params, functionalSelectIds: params.selectIds });
} else {
try {
await props.saveApi({
functionalSelectIds: params.selectIds,
testPlanId: planId.value,
});
emit('success', { ...params, functionalSelectIds: params.selectIds });
Message.success(t('ms.case.associate.associateSuccess'));
confirmLoading.value = false;
} catch (error) {
console.log(error);
}
Message.success(t('ms.case.associate.associateSuccess'));
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
confirmLoading.value = false;
}
innerVisible.value = false;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
confirmLoading.value = false;
}
innerVisible.value = false;
}
</script>

View File

@ -0,0 +1,493 @@
<template>
<MsMinderEditor
v-model:extra-visible="extraVisible"
v-model:loading="loading"
v-model:import-json="importJson"
:tags="[]"
:insert-node="insertNode"
:can-show-enter-node="false"
:insert-sibling-menus="insertSiblingMenus"
:insert-son-menus="insertSonMenus"
:can-show-paste-menu="false"
:can-show-more-menu="false"
:can-show-priority-menu="false"
:can-show-float-menu="canShowFloatMenu"
custom-priority
single-tag
tag-enable
sequence-enable
@content-change="handleContentChange"
@node-select="checkNodeCanShowMenu"
@before-exec-command="handleBeforeExecCommand"
@save="handleMinderSave"
>
<template #extractMenu>
<a-dropdown
v-if="canShowExecuteMethodMenu"
v-model:popup-visible="executeMethodMenuVisible"
class="ms-minder-dropdown"
:popup-translate="[0, 4]"
position="bl"
trigger="click"
@select="(val) => handleExecuteMethodMenuSelect(val as RunMode)"
>
<a-tooltip :content="t('ms.minders.executeMethod')">
<MsButton
type="icon"
class="ms-minder-node-float-menu-icon-button"
:class="[executeMethodMenuVisible ? 'ms-minder-node-float-menu-icon-button--focus' : '']"
>
<MsIcon type="icon-icon_play-round_filled" class="text-[var(--color-text-4)]" />
</MsButton>
</a-tooltip>
<template #content>
<div class="mx-[6px] px-[8px] py-[3px] text-[var(--color-text-4)]">
{{ t('ms.minders.executeMethod') }}
</div>
<a-doption :value="RunMode.SERIAL">
<div
class="flex h-[20px] w-[20px] items-center justify-center rounded-full bg-[rgb(var(--link-1))] text-[12px] font-medium text-[rgb(var(--link-5))]"
>
{{ t('ms.minders.serial') }}
</div>
</a-doption>
<a-doption :value="RunMode.PARALLEL">
<div
class="flex h-[20px] w-[20px] items-center justify-center rounded-full bg-[rgb(var(--success-1))] text-[12px] font-medium text-[rgb(var(--success-6))]"
>
{{ t('ms.minders.parallel') }}
</div>
</a-doption>
</template>
</a-dropdown>
<a-tooltip v-if="showConfigMenu" :content="t('common.config')">
<MsButton
type="icon"
class="ms-minder-node-float-menu-icon-button"
:class="[extraVisible ? 'ms-minder-node-float-menu-icon-button--focus' : '']"
@click="toggleDetail"
>
<MsIcon type="icon-icon_setting_filled" class="text-[var(--color-text-4)]" />
</MsButton>
</a-tooltip>
</template>
<template #extractTabContent>
<div class="px-[16px]">
<a-form ref="configFormRef" :model="configForm" layout="vertical">
<a-form-item>
<template #label>
<div class="flex items-center">
<div>{{ t('testPlan.planForm.pickCases') }}</div>
<a-divider margin="4px" direction="vertical" />
<MsButton
type="text"
:disabled="
(selectedAssociateCasesParams.totalCount || selectedAssociateCasesParams.selectIds.length) === 0
"
@click="clearSelectedCases"
>
{{ t('caseManagement.caseReview.clearSelectedCases') }}
</MsButton>
</div>
</template>
<div class="bg-[var(--color-text-n9)] p-[12px]">
<div class="flex items-center">
<div class="text-[var(--color-text-2)]">
{{
t('caseManagement.caseReview.selectedCases', {
count: selectedAssociateCasesParams.selectAll
? selectedAssociateCasesParams.totalCount
: selectedAssociateCasesParams.selectIds.length,
})
}}
</div>
<a-divider margin="8px" direction="vertical" />
<MsButton
v-permission="['CASE_REVIEW:READ+RELEVANCE']"
type="text"
class="font-medium"
@click="caseAssociateVisible = true"
>
{{ t('ms.case.associate.title') }}
</MsButton>
</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">
<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>
</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.retrySpace"
mode="button"
:step="100"
:min="0"
:precision="0"
size="small"
class="w-[120px]"
></a-input-number>
</a-form-item>
</template>
<a-form-item class="hidden-item">
<div class="flex items-center gap-[8px]">
<a-switch v-model:model-value="configForm.extend" size="small"></a-switch>
<div>{{ t('ms.minders.extend') }}</div>
</div>
</a-form-item>
</a-form>
</div>
</template>
</MsMinderEditor>
<caseAssociate
v-model:visible="caseAssociateVisible"
v-model:currentSelectCase="currentSelectCase"
:has-not-associated-ids="selectedAssociateCasesParams.selectIds"
test-plan-id=""
@success="writeAssociateCases"
/>
</template>
<script setup lang="ts">
import { FormInstance, SelectOptionData } from '@arco-design/web-vue';
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 { setCustomPriorityView } from '@/components/pure/ms-minder-editor/script/tool/utils';
import caseAssociate from './associateDrawer.vue';
import { 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 { AssociateCaseRequest } from '@/models/testPlan/testPlan';
import { CaseLinkEnum } from '@/enums/caseEnum';
import { RunMode } from '@/enums/testPlanEnum';
import Message from '@arco-design/web-vue/es/message';
const props = defineProps<{
planId: string;
}>();
const appStore = useAppStore();
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: [],
});
/**
* 插入节点
* @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 insertSiblingMenus = ref<InsertMenuItem[]>([]);
const insertSonMenus = ref<InsertMenuItem[]>([]);
const canShowFloatMenu = ref(false);
const canShowExecuteMethodMenu = ref(true);
const executeMethodMenuVisible = ref(false);
const showConfigMenu = ref(false);
/**
* 检测节点可展示的菜单项
* @param node 选中节点
*/
function checkNodeCanShowMenu(node: MinderJsonNode) {
const { data } = node;
if (data?.level === 1 || data?.level === 2) {
canShowFloatMenu.value = true;
canShowExecuteMethodMenu.value = true;
if (data?.level === 1) {
insertSiblingMenus.value = [];
insertSonMenus.value = [
{
value: 'testSet',
label: t('ms.minders.testSet'),
},
];
showConfigMenu.value = true;
} else {
insertSiblingMenus.value = [];
insertSonMenus.value = [];
}
} else {
canShowFloatMenu.value = false;
canShowExecuteMethodMenu.value = false;
}
}
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,
};
function handleExecuteMethodMenuSelect(val: RunMode) {
currentPriority.value = val;
window.minder.execCommand('priority', priorityMap[val]);
setCustomPriorityView(priorityTextMap);
}
/**
* 切换用例详情显示
*/
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('')) {
console.log();
}
}
}
/**
* 是否停止拖拽排序动作
* @param dragNode 拖动节点
* @param dropNode 目标节点
*/
function stopArrangeDrag(dragNodes: MinderJsonNode | MinderJsonNode[], dropNode: MinderJsonNode) {
if (!Array.isArray(dragNodes)) {
dragNodes = [dragNodes];
}
for (let i = 0; i < dragNodes.length; i++) {
const dragNode = (dragNodes as MinderJsonNode[])[i];
}
return true;
}
/**
* 脑图命令执行前拦截
* @param event 命令执行事件
*/
function handleBeforeExecCommand(event: MinderEvent) {
if (event.commandName === 'movetoparent') {
//
event.stopPropagation();
} else if (event.commandName === 'arrange') {
//
const dragNodes: MinderJsonNode[] = window.minder.getSelectedNodes();
let dropNode: MinderJsonNode;
if (dragNodes[0].parent?.children?.[event.commandArgs[0] as number]) {
//
dropNode = dragNodes[0].parent?.children?.[event.commandArgs[0] as number];
} else if (dragNodes[0].parent?.children?.[(event.commandArgs[0] as number) - 1]) {
//
dropNode = dragNodes[0].parent?.children?.[(event.commandArgs[0] as number) - 1];
} else {
//
dropNode = dragNodes[dragNodes.length - 1];
}
if (stopArrangeDrag(dragNodes, dropNode)) {
event.stopPropagation();
}
}
}
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: '',
};
}
/**
* 初始化测试规划脑图
*/
async function initMinder() {
try {
loading.value = true;
const res = await getPlanMinder(props.planId);
[importJson.value.root] = mapTree(res, (node, path, level) => {
node.data = {
...node.data,
level,
isNew: false,
changed: false,
};
return node;
});
window.minder.importJson(importJson.value);
window.minder.execCommand('template', Object.keys(window.kityminder.Minder.getTemplateList())[3]);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
loading.value = false;
}
}
/**
* 生成脑图保存的入参
*/
function makeMinderParams(fullJson: MinderJson) {
filterTree(fullJson.root.children, (node, nodeIndex, parent) => {
if (node.data.isNew !== false || node.data.changed === true) {
return true;
}
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'));
initMinder();
callback();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
loading.value = false;
}
}
onMounted(() => {
initMinder();
nextTick(() => {
window.minder.on('contentchange', () => {
//
setTimeout(() => {
setCustomPriorityView(priorityTextMap);
}, 0);
});
window.minder.on('selectionchange', () => {
setTimeout(() => {
setCustomPriorityView(priorityTextMap);
}, 0);
});
});
});
</script>
<style lang="less" scoped>
:deep(.arco-form-item) {
margin-bottom: 16px;
}
</style>

View File

@ -0,0 +1,15 @@
export default {
'ms.minders.failStop': '失败停止',
'ms.minders.failRetry': '失败重试',
'ms.minders.stepRetry': '步骤重试',
'ms.minders.scenarioRetry': '场景重试',
'ms.minders.retry': '重试',
'ms.minders.retryTimes': '(次)',
'ms.minders.retrySpace': '每次间隔',
'ms.minders.retrySpaces': '(ms)',
'ms.minders.extend': '继承上级配置',
'ms.minders.testSet': '测试集',
'ms.minders.executeMethod': '运行方式',
'ms.minders.serial': '串',
'ms.minders.parallel': '并',
};

View File

@ -0,0 +1,25 @@
<template>
<a-alert v-if="!getIsVisited()" :show-icon="false" :type="props.type" closable @close="addVisited">
<slot>{{ t(props.tip || '') }}</slot>
<template #close-element>
<span class="text-[14px]">{{ t('common.notRemind') }}</span>
</template>
</a-alert>
</template>
<script setup lang="ts">
import { useI18n } from '@/hooks/useI18n';
import useVisit from '@/hooks/useVisit';
const props = defineProps<{
tip?: string;
type?: 'error' | 'normal' | 'success' | 'warning' | 'info';
visitedKey: string;
}>();
const { t } = useI18n();
const { addVisited, getIsVisited } = useVisit(props.visitedKey);
</script>
<style lang="less" scoped></style>

View File

@ -12,7 +12,7 @@
</a-breadcrumb-item>
</a-breadcrumb>
</div>
<nodeFloatMenu v-bind="props">
<nodeFloatMenu v-if="props.canShowFloatMenu" v-bind="props">
<template #extractMenu>
<slot name="extractMenu"></slot>
</template>

View File

@ -36,7 +36,7 @@
const commandValue = ref('');
const commandDisabled = ref(true);
let minder = reactive<any>({});
const minder = reactive<any>({});
const isDisable = (): boolean => {
if (Object.keys(minder).length === 0) return true;
@ -58,23 +58,23 @@
return !!minder.queryCommandState && minder.queryCommandState('priority') === -1;
};
onMounted(() => {
nextTick(() => {
minder = window.minder;
const freshFuc = setPriorityView;
if (minder.on) {
minder.on('contentchange', () => {
//
setTimeout(() => {
freshFuc(props.priorityStartWithZero, props.priorityPrefix);
}, 0);
});
minder.on('selectionchange', () => {
commandDisabled.value = isDisable();
});
}
});
});
// onMounted(() => {
// nextTick(() => {
// minder = window.minder;
// const freshFuc = setPriorityView;
// if (minder.on) {
// minder.on('contentchange', () => {
// //
// setTimeout(() => {
// freshFuc(props.priorityStartWithZero, props.priorityPrefix);
// }, 0);
// });
// minder.on('selectionchange', () => {
// commandDisabled.value = isDisable();
// });
// }
// });
// });
function execCommand(index?: number) {
if (index && minder.execCommand) {

View File

@ -233,6 +233,9 @@
} else {
menuVisible.value = false;
}
},
{
immediate: true,
}
);
@ -376,7 +379,7 @@
onMounted(() => {
nextTick(() => {
const freshFuc = setPriorityView;
if (window.minder) {
if (window.minder && !props.customPriority) {
window.minder.on('contentchange', () => {
//
setTimeout(() => {

View File

@ -7,16 +7,14 @@
</template>
</mainEditor>
</div>
<template v-if="props.extractContentTabList?.length">
<div class="ms-minder-editor-extra" :class="[extraVisible ? 'ms-minder-editor-extra--visible' : '']">
<div class="pl-[16px] pt-[16px]">
<MsTab v-model:activeKey="activeExtraKey" :content-tab-list="props.extractContentTabList" mode="button" />
</div>
<div class="ms-minder-editor-extra-content">
<slot name="extractTabContent"></slot>
</div>
<div class="ms-minder-editor-extra" :class="[extraVisible ? 'ms-minder-editor-extra--visible' : '']">
<div v-if="props.extractContentTabList?.length" class="pl-[16px] pt-[16px]">
<MsTab v-model:activeKey="activeExtraKey" :content-tab-list="props.extractContentTabList" mode="button" />
</div>
</template>
<div class="ms-minder-editor-extra-content">
<slot name="extractTabContent"></slot>
</div>
</div>
</a-spin>
</template>

View File

@ -16,7 +16,7 @@ export interface MinderJsonNodeData {
text: string;
resource?: string[];
expandState?: 'collapse' | 'expand';
priority?: number;
priority?: number | string;
// 前端渲染字段
isNew?: boolean; // 是否脑图新增节点,需要在初始化脑图数据时标记已存在节点为 false 以区分是否新增节点
changed?: boolean; // 脑图节点是否发生过变化
@ -168,6 +168,16 @@ export const floatMenuProps = {
replaceableTags: {
type: Function as PropType<(nodes: MinderJsonNode[]) => string[]>,
},
// 是否显示浮动菜单
canShowFloatMenu: {
type: Boolean,
default: true,
},
// 是否自定义优先级
customPriority: {
type: Boolean,
default: false,
},
};
export const insertProps = {

View File

@ -99,6 +99,26 @@ export function setPriorityView(priorityStartWithZero: boolean, priorityPrefix:
}
}
/**
*
* @param valueMap
*/
export function setCustomPriorityView(valueMap: Record<any, string>) {
const items = document.getElementsByTagName('text');
if (items) {
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (isPriority(item)) {
const content = item.innerHTML;
if (valueMap[content]) {
// 检查当前节点内优先级文本是否在映射中,如果在则替换;否则代表已经被替换过了,不再处理
item.innerHTML = valueMap[content];
}
}
}
}
}
/**
* id置为nullchanged true
* @param node

View File

@ -4,4 +4,18 @@ export enum testPlanTypeEnum {
GROUP = 'GROUP',
}
export default {};
export enum RunMode {
SERIAL = 'SERIAL', // 串行
PARALLEL = 'PARALLEL', // 并行
}
export enum TestSetType {
FUNCTIONAL_CASE = 'FUNCTIONAL_CASE',
API_CASE = 'API_CASE',
SCENARIO_CASE = 'SCENARIO_CASE',
}
export enum FailRetry {
STEP = 'STEP',
SCENARIO = 'SCENARIO',
}

View File

@ -187,4 +187,5 @@ export default {
'common.noMatchData': 'No matching data',
'common.name': 'name',
'common.stopped': 'Stopped',
'common.config': 'Config',
};

View File

@ -188,4 +188,5 @@ export default {
'common.noMatchData': '暂无匹配数据',
'common.name': '名称',
'common.stopped': '已停止',
'common.config': '配置',
};

View File

@ -1,10 +1,11 @@
import type { 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 { testPlanTypeEnum } from '@/enums/testPlanEnum';
import { type FailRetry, type RunMode, testPlanTypeEnum, type TestSetType } from '@/enums/testPlanEnum';
export type planStatusType = 'PREPARED' | 'UNDERWAY' | 'COMPLETED' | 'ARCHIVED';
@ -352,4 +353,26 @@ export interface ExecutePlan {
executeIds: string[];
executeMode: RunModeType;
}
export default {};
export interface PlanMinderNodeData extends MinderJsonNodeData {
id: string;
pos: number;
text: string;
num: number; // 关联用例数量
priority: string; // 串行/并行
executeMethod: RunMode; // 串行/并行值
type: TestSetType; // 测试集类型(功能/接口/场景)
extended: boolean;
grouped: boolean; // 是否使用环境组
environmentId: string;
testResourcePoolId: string;
retryOnFail: boolean;
retryType: FailRetry; // 失败重试类型(步骤/场景)
retryTimes: number;
retryInterval: number;
stopOnFail: boolean;
}
export interface PlanMinderNode {
data: PlanMinderNodeData;
children: PlanMinderNode[];
}

View File

@ -233,7 +233,7 @@ export function traverseTree<T>(
*/
export function mapTree<T>(
tree: TreeNode<T> | TreeNode<T>[] | T | T[],
customNodeFn: (node: TreeNode<T>, path: string) => TreeNode<T> | null = (node) => node,
customNodeFn: (node: TreeNode<T>, path: string, _level: number) => TreeNode<T> | null = (node) => node,
customChildrenKey = 'children',
parentPath = '',
level = 0,
@ -258,7 +258,7 @@ export function mapTree<T>(
const fullPath = node.path ? `${_parentPath}/${node.path}`.replace(/\/+/g, '/') : '';
node.sort = i + 1; // sort 从 1 开始
node.parent = _parent || undefined; // 没有父节点说明是树的第一层
const newNode = typeof customNodeFn === 'function' ? customNodeFn(node, fullPath) : node;
const newNode = typeof customNodeFn === 'function' ? customNodeFn(node, fullPath, _level) : node;
if (newNode) {
newNode.level = _level;
if (newNode[customChildrenKey] && newNode[customChildrenKey].length > 0) {

View File

@ -1,11 +1,11 @@
<template>
<div>
<a-alert v-if="!getIsVisited()" :show-icon="false" class="mb-[16px]" type="warning" closable @close="addVisited">
{{ t('apiTestManagement.historyListTip') }}
<template #close-element>
<span class="text-[14px]">{{ t('common.notRemind') }}</span>
</template>
</a-alert>
<MsNotRemind
tip="apiTestManagement.historyListTip"
class="mb-[16px]"
type="warning"
visited-key="messageManagementRobotListTip"
/>
<ms-base-table v-bind="propsRes" no-disable v-on="propsEvent">
<!-- <template #action="{ record }">
<div class="flex items-center">
@ -23,11 +23,11 @@
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import MsNotRemind from '@/components/business/ms-not-remind/index.vue';
import { operationHistory } from '@/api/modules/api-test/management';
import { operationTypeOptions } from '@/config/common';
import { useI18n } from '@/hooks/useI18n';
import useVisit from '@/hooks/useVisit';
import useAppStore from '@/store/modules/app';
const props = defineProps<{
@ -36,8 +36,6 @@
const appStore = useAppStore();
const { t } = useI18n();
const visitedKey = 'messageManagementRobotListTip';
const { addVisited, getIsVisited } = useVisit(visitedKey);
const columns: MsTableColumn = [
{

View File

@ -1,11 +1,11 @@
<template>
<div class="history-container">
<a-alert v-if="!getIsVisited()" :show-icon="false" class="mb-[16px]" type="warning" closable @close="addVisited">
{{ t('apiTestManagement.historyListTip') }}
<template #close-element>
<span class="text-[14px]">{{ t('common.notRemind') }}</span>
</template>
</a-alert>
<MsNotRemind
tip="apiTestManagement.historyListTip"
class="mb-[16px]"
type="warning"
visited-key="apiTestCaseChangeHistoryTip"
/>
<ms-base-table v-bind="propsRes" no-disable v-on="propsEvent" @filter-change="filterChange"> </ms-base-table>
</div>
</template>
@ -16,11 +16,11 @@
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import MsNotRemind from '@/components/business/ms-not-remind/index.vue';
import { getApiCaseChangeHistory } from '@/api/modules/api-test/management';
import { operationTypeOptions } from '@/config/common';
import { useI18n } from '@/hooks/useI18n';
import useVisit from '@/hooks/useVisit';
import useAppStore from '@/store/modules/app';
import { hasAnyPermission } from '@/utils/permission';
@ -33,8 +33,6 @@
const appStore = useAppStore();
const { t } = useI18n();
const visitedKey = 'messageManagementRobotListTip';
const { addVisited, getIsVisited } = useVisit(visitedKey);
const typeOptions = [
{

View File

@ -1,11 +1,6 @@
<template>
<div>
<a-alert v-if="!getIsVisited()" :show-icon="false" class="mb-[16px]" type="warning" closable @close="addVisited">
{{ t('apiScenario.historyListTip') }}
<template #close-element>
<span class="text-[14px]">{{ t('common.notRemind') }}</span>
</template>
</a-alert>
<MsNotRemind tip="apiScenario.historyListTip" class="mb-[16px]" type="warning" visited-key="scenarioHistoryTip" />
<ms-base-table v-bind="propsRes" no-disable v-on="propsEvent"></ms-base-table>
</div>
</template>
@ -16,17 +11,15 @@
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import MsNotRemind from '@/components/business/ms-not-remind/index.vue';
import { getScenarioHistory } from '@/api/modules/api-test/scenario';
import { operationTypeOptions } from '@/config/common';
import { useI18n } from '@/hooks/useI18n';
import useVisit from '@/hooks/useVisit';
import useAppStore from '@/store/modules/app';
const appStore = useAppStore();
const { t } = useI18n();
const visitedKey = 'scenarioHistoryTip';
const { addVisited, getIsVisited } = useVisit(visitedKey);
const props = defineProps<{
sourceId?: string | number;
}>();

View File

@ -70,34 +70,6 @@
<a-form-item field="tags" :label="t('common.tag')" class="w-[436px]">
<MsTagsInput v-model:model-value="form.tags" :max-tag-count="10" />
</a-form-item>
<a-form-item v-if="!props.planId?.length">
<template #label>
<div class="flex items-center">
{{ t('testPlan.planForm.pickCases') }}
<a-divider margin="4px" direction="vertical" />
<MsButton
type="text"
:disabled="form.baseAssociateCaseRequest?.selectIds.length === 0"
@click="clearSelectedCases"
>
{{ t('caseManagement.caseReview.clearSelectedCases') }}
</MsButton>
</div>
</template>
<div class="flex w-[436px] items-center rounded bg-[var(--color-text-n9)] p-[12px]">
<div class="text-[var(--color-text-2)]">
{{
t('caseManagement.caseReview.selectedCases', {
count: getSelectedCount,
})
}}
</div>
<a-divider margin="8px" direction="vertical" />
<MsButton type="text" class="font-medium" @click="caseAssociateVisible = true">
{{ t('ms.case.associate.title') }}
</MsButton>
</div>
</a-form-item>
<MsMoreSettingCollapse>
<template #content>
<div v-for="item in switchList" :key="item.key" class="mb-[24px] flex items-center gap-[8px]">
@ -134,11 +106,6 @@
</MsMoreSettingCollapse>
</a-form>
</MsDrawer>
<AssociateDrawer
v-model:visible="caseAssociateVisible"
:has-not-associated-ids="form.baseAssociateCaseRequest?.selectIds"
@success="writeAssociateCases"
/>
</template>
<script setup lang="ts">
@ -147,18 +114,16 @@
import { cloneDeep } from 'lodash-es';
import dayjs from 'dayjs';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsMoreSettingCollapse from '@/components/pure/ms-more-setting-collapse/index.vue';
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
import AssociateDrawer from './components/associateDrawer.vue';
import { addTestPlan, getTestPlanDetail, updateTestPlan } from '@/api/modules/test-plan/testPlan';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { ModuleTreeNode } from '@/models/common';
import type { AddTestPlanParams, AssociateCaseRequest, SwitchListModel } from '@/models/testPlan/testPlan';
import type { AddTestPlanParams, SwitchListModel } from '@/models/testPlan/testPlan';
import { testPlanTypeEnum } from '@/enums/testPlanEnum';
import { DisabledTimeProps } from '@arco-design/web-vue/es/date-picker/interface';
@ -267,14 +232,6 @@
},
];
const caseAssociateVisible = ref(false);
function clearSelectedCases() {
form.value.baseAssociateCaseRequest = cloneDeep(initForm.baseAssociateCaseRequest);
}
function writeAssociateCases(param: AssociateCaseRequest) {
form.value.baseAssociateCaseRequest = { ...param };
}
function handleCancel() {
innerVisible.value = false;
formRef.value?.resetFields();
@ -351,13 +308,4 @@
const okText = computed(() => {
return props.planId ? t('common.update') : t('common.create');
});
const getSelectedCount = computed(() => {
if (props.planId) {
return form.value?.functionalCaseCount || 0;
}
return form.value.baseAssociateCaseRequest?.selectAll
? form.value.baseAssociateCaseRequest?.totalCount
: form.value.baseAssociateCaseRequest?.selectIds.length;
});
</script>

View File

@ -29,15 +29,6 @@
@change="loadActiveTabList"
/>
<span class="mr-[14px]">{{ t('testPlan.testPlanDetail.moduleView') }}</span>
<MsButton
v-if="hasAnyPermission(['PROJECT_TEST_PLAN:READ+ASSOCIATION']) && detail.status !== 'ARCHIVED'"
type="button"
status="default"
@click="linkCase"
>
<MsIcon type="icon-icon_link-record_outlined1" class="mr-[8px]" />
{{ t('ms.case.associate.title') }}
</MsButton>
<MsButton v-if="isEnableEdit" type="button" status="default" @click="editorCopyHandler(false)">
<MsIcon type="icon-icon_edit_outlined" class="mr-[8px]" />
{{ t('common.edit') }}
@ -110,6 +101,7 @@
</MsCard>
<!-- special-height的174: 上面卡片高度158 + mt的16 -->
<MsCard class="mt-[16px]" :special-height="174" simple has-breadcrumb no-content-padding>
<Plan v-if="activeTab === 'plan'" :plan-id="planId" />
<FeatureCase
v-if="activeTab === 'featureCase'"
ref="featureCaseRef"
@ -135,15 +127,6 @@
/>
<ExecuteHistory v-if="activeTab === 'executeHistory'" />
</MsCard>
<!-- TODO 待联调关联用例 目前可以暂时关联功能用例 -->
<AssociateDrawer
v-model:visible="caseAssociateVisible"
:associated-ids="detail.repeatCase ? hasSelectedIds : []"
:save-api="associationCaseToPlan"
:test-plan-id="planId"
@success="handleSuccess"
/>
<CreateAndEditPlanDrawer
v-model:visible="showPlanDrawer"
:plan-id="planId"
@ -167,18 +150,17 @@
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import MsStatusTag from '@/components/business/ms-status-tag/index.vue';
import ActionModal from '../components/actionModal.vue';
import AssociateDrawer from '../components/associateDrawer.vue';
import StatusProgress from '../components/statusProgress.vue';
import ApiCase from './apiCase/index.vue';
import ApiScenario from './apiScenario/index.vue';
import BugManagement from './bugManagement/index.vue';
import ExecuteHistory from './executeHistory/index.vue';
import FeatureCase from './featureCase/index.vue';
import Plan from './plan/index.vue';
import CreateAndEditPlanDrawer from '@/views/test-plan/testPlan/createAndEditPlanDrawer.vue';
import {
archivedPlan,
associationCaseToPlan,
followPlanRequest,
generateReport,
getPlanPassRate,
@ -317,8 +299,12 @@
}
}
const activeTab = ref('featureCase');
const activeTab = ref('plan');
const tabList = ref([
{
value: 'plan',
label: t('testPlan.plan'),
},
{
value: 'featureCase',
label: t('menu.caseManagement.featureCase'),
@ -358,12 +344,7 @@
return '';
}
}
const hasSelectedIds = ref<string[]>([]);
const caseAssociateVisible = ref(false);
//
function linkCase() {
caseAssociateVisible.value = true;
}
const showPlanDrawer = ref(false);
//

View File

@ -0,0 +1,21 @@
<template>
<div class="flex h-full flex-col">
<div class="p-[16px]">
<MsNotRemind tip="testPlan.planTip" type="info" visited-key="testPlanTip" />
</div>
<div class="flex-1 overflow-hidden px-[16px]">
<MsTestPlanMinder :plan-id="props.planId" />
</div>
</div>
</template>
<script setup lang="ts">
import MsTestPlanMinder from '@/components/business/ms-minders/testPlanMinder/index.vue';
import MsNotRemind from '@/components/business/ms-not-remind/index.vue';
const props = defineProps<{
planId: string;
}>();
</script>
<style lang="less" scoped></style>

View File

@ -137,4 +137,7 @@ export default {
'testPlan.testPlanGroup.deleteScheduleTaskSuccess': 'Delete the scheduled task successfully',
'testPlan.testPlanGroup.enableScheduleTaskSuccess': 'Start the scheduled task successfully',
'testPlan.testPlanGroup.closeScheduleTaskSuccess': 'Scheduled mission closed successfully',
'testPlan.plan': 'Test plan',
'testPlan.planTip':
'1. Create a test set for business classification testing; 2. Select the test set associated use case',
};

View File

@ -126,4 +126,6 @@ export default {
'testPlan.testPlanGroup.deleteScheduleTaskSuccess': '删除定时任务成功',
'testPlan.testPlanGroup.enableScheduleTaskSuccess': '开启定时任务成功',
'testPlan.testPlanGroup.closeScheduleTaskSuccess': '关闭定时任务成功',
'testPlan.plan': '测试规划',
'testPlan.planTip': '1.创建测试集进行业务分类测试2.选择测试集关联用例',
};