feat(用例管理): 用例详情新增测试计划tab页

This commit is contained in:
guoyuqi 2024-05-09 16:44:40 +08:00 committed by 刘瑞斌
parent 4f97fd5428
commit 7478a1dff0
8 changed files with 263 additions and 29 deletions

View File

@ -8,9 +8,12 @@ import lombok.Data;
public class FunctionalCaseTestPlanDTO extends TestPlanFunctionalCase {
@Schema(description = "测试计划ID")
private String testPlanNum;
private Long testPlanNum;
@Schema(description = "所属项目")
@Schema(description = "测试计划名称")
private String testPlanName;
@Schema(description = "所属项目名称")
private String projectName;
@Schema(description = "计划状态")

View File

@ -58,7 +58,7 @@
<select id="getPlanList" parameterType="io.metersphere.functional.request.AssociatePlanPageRequest" resultType="io.metersphere.functional.dto.FunctionalCaseTestPlanDTO">
SELECT
tpfc.*, tp.num as testPlanNum, tp.status as planStatus, p.name as projectName
tpfc.*, tp.name as testPlanName, tp.num as testPlanNum, tp.status as planStatus, p.name as projectName
FROM
test_plan_functional_case tpfc
LEFT JOIN test_plan tp ON tpfc.test_plan_id = tp.id
@ -71,6 +71,9 @@
OR tp.num LIKE concat('%', #{request.keyword},'%')
)
</if>
<include refid="planFilters">
<property name="filter" value="request.filter"/>
</include>
</select>
<sql id="queryWhereConditionByBaseQueryRequest">
@ -133,5 +136,22 @@
</if>
</sql>
<sql id="planFilters">
<if test="${filter} != null and ${filter}.size() > 0">
<foreach collection="${filter}.entrySet()" index="key" item="values">
<if test="values != null and values.size() > 0">
<choose>
<when test="key=='planStatus'">
AND tp.status in
<include refid="io.metersphere.system.mapper.BaseMapper.filterInWrapper"/>
</when>
<when test="key=='lastExecResult'">
AND tpfc.last_exec_result in
<include refid="io.metersphere.system.mapper.BaseMapper.filterInWrapper"/>
</when>
</choose>
</if>
</foreach>
</if>
</sql>
</mapper>

View File

@ -36,6 +36,7 @@ import {
GetAssociatedDebuggerUrl,
GetAssociatedDrawerCaseUrl,
GetAssociatedFilePageUrl,
GetAssociatedTestPlanUrl,
GetAssociationPublicCaseModuleCountUrl,
GetAssociationPublicCasePageUrl,
GetAssociationPublicModuleTreeUrl,
@ -96,6 +97,7 @@ import type {
} from '@/models/caseManagement/featureCase';
import type { CommonList, ModuleTreeNode, MoveModules, TableQueryParams } from '@/models/common';
import { ProjectListItem } from '@/models/setting/project';
import { AssociateFunctionalCaseItem, TestPlanItem } from '@/models/testPlan/testPlan';
// 获取模块树
export function getCaseModuleTree(params: TableQueryParams) {
@ -428,4 +430,9 @@ export function getAssociatedProjectOptions(orgId: string, module: string) {
return MSR.get<ProjectListItem[]>({ url: `${associatedProjectOptionsUrl}/${orgId}/${module}` });
}
// 获取已关联测试计划列表
export function getLinkedCaseTestPlanList(data: TableQueryParams) {
return MSR.post<CommonList<AssociateFunctionalCaseItem>>({ url: GetAssociatedTestPlanUrl, data });
}
export default {};

View File

@ -151,3 +151,6 @@ export const getChangeHistoryListUrl = '/functional/case/operation-history';
export const cancelDisassociate = '/functional/case/test/disassociate/case';
// 关联用例关联功能用例项目下拉
export const associatedProjectOptionsUrl = '/project/list/options';
// 获取详情已关联测试计划列表
export const GetAssociatedTestPlanUrl = '/functional/case/test/has/associate/plan/page';

View File

@ -21,6 +21,14 @@ export interface TestPlanItem {
groupId: string;
}
export interface AssociateFunctionalCaseItem {
testPlanId: string;
testPlanNum: number;
testPlanName: string;
projectName: string;
planStatus: string;
}
export interface ResourcesItem {
id: string;
name: string;

View File

@ -141,7 +141,7 @@
<TabCaseReview :case-id="props.detailId" />
</template>
<template v-if="activeTab === 'testPlan'">
<TabTestPlan />
<TabTestPlan :case-id="props.detailId" />
</template>
<template v-if="activeTab === 'comments'">
<TabComment ref="commentRef" :case-id="props.detailId" />
@ -666,6 +666,12 @@
canHide: true,
isShow: true,
},
{
value: 'testPlan',
label: t('caseManagement.featureCase.testPlan'),
canHide: true,
isShow: true,
},
{
value: 'comments',
label: t('caseManagement.featureCase.comments'),

View File

@ -7,14 +7,101 @@
:placeholder="t('caseManagement.featureCase.searchByNameAndId')"
allow-clear
class="mx-[8px] w-[240px]"
@search="searchList"
@press-enter="searchList"
@clear="searchList"
></a-input-search>
</div>
<ms-base-table v-bind="propsRes" v-on="propsEvent">
<template #name="{ record }">
<a-button type="text" class="px-0">{{ record.name }}</a-button>
<template #testPlanNum="{ record }">
<a-button type="text" class="px-0" @click="goToPlan(record)">{{ record.testPlanNum }}</a-button>
</template>
<template #status="{ record }">
<statusTag :status="record.status" />
<template #planStatus="{ record }">
<statusTag :status="record.planStatus" />
</template>
<template #statusFilter="{ columnConfig }">
<a-trigger
v-model:popup-visible="statusFilterVisible"
trigger="click"
@popup-visible-change="handleFilterHidden"
>
<a-button
type="text"
class="arco-btn-text--secondary p-[8px_4px] text-[14px]"
@click="statusFilterVisible = true"
>
{{ t(columnConfig.title as string) }}
<icon-down :class="statusFilterVisible ? 'text-[rgb(var(--primary-5))]' : ''" />
</a-button>
<template #content>
<div class="arco-table-filters-content">
<div class="flex items-center justify-center px-[6px] py-[2px]">
<a-checkbox-group v-model:model-value="statusFilters" direction="vertical" size="small">
<a-checkbox v-for="key of Object.keys(planStatusMap)" :key="key" :value="key">
<a-tag
:color="planStatusMap[key as planStatusType].color"
:class="[planStatusMap[key as planStatusType].class, 'px-[4px]']"
size="small"
>
{{ t(planStatusMap[key as planStatusType].label) }}
</a-tag>
</a-checkbox>
</a-checkbox-group>
</div>
<div class="filter-button">
<a-button size="mini" class="mr-[8px]" @click="resetStatusFilter">
{{ t('common.reset') }}
</a-button>
<a-button type="primary" size="mini" @click="handleFilterHidden(false)">
{{ t('system.orgTemplate.confirm') }}
</a-button>
</div>
</div>
</template>
</a-trigger>
</template>
<template #lastExecResult="{ record }">
<execute-result :execute-result="record.lastExecResult" />
</template>
<template #lastExecResultFilter="{ columnConfig }">
<a-trigger
v-model:popup-visible="lastExecResultVisible"
trigger="click"
@popup-visible-change="handleFilterHidden"
>
<a-button
type="text"
class="arco-btn-text--secondary p-[8px_4px] text-[14px]"
@click="lastExecResultVisible = true"
>
{{ t(columnConfig.title as string) }}
<icon-down :class="lastExecResultVisible ? 'text-[rgb(var(--primary-5))]' : ''" />
</a-button>
<template #content>
<div class="arco-table-filters-content">
<div class="flex items-center justify-center px-[6px] py-[2px]">
<a-checkbox-group v-model:model-value="lastExecResultFilters" direction="vertical" size="small">
<a-checkbox v-for="key of Object.keys(executionResultMap)" :key="key" :value="key">
<MsIcon
:type="executionResultMap[key]?.icon || ''"
class="mr-1"
:class="[executionResultMap[key].color]"
></MsIcon>
<span>{{ executionResultMap[key]?.statusText || '' }} </span>
</a-checkbox>
</a-checkbox-group>
</div>
<div class="filter-button">
<a-button size="mini" class="mr-[8px]" @click="resetLastExecuteResultFilter">
{{ t('common.reset') }}
</a-button>
<a-button type="primary" size="mini" @click="handleFilterHidden(false)">
{{ t('system.orgTemplate.confirm') }}
</a-button>
</div>
</div>
</template>
</a-trigger>
</template>
</ms-base-table>
</div>
@ -22,34 +109,48 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { debounce } from 'lodash-es';
import dayjs from 'dayjs';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import ExecuteResult from '@/components/business/ms-case-associate/executeResult.vue';
import statusTag from '@/views/case-management/caseReview/components/statusTag.vue';
import { getRecycleListRequest } from '@/api/modules/case-management/featureCase';
import { getLinkedCaseTestPlanList } from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import useFeatureCaseStore from '@/store/modules/case/featureCase';
import { AssociateFunctionalCaseItem, planStatusType } from '@/models/testPlan/testPlan';
import { TestPlanRouteEnum } from '@/enums/routeEnum';
import { TableKeyEnum } from '@/enums/tableEnum';
const featureCaseStore = useFeatureCaseStore();
const activeTab = computed(() => featureCaseStore.activeTab);
import { executionResultMap } from '@/views/case-management/caseManagementFeature/components/utils';
import { planStatusMap } from '@/views/test-plan/testPlan/config';
const { t } = useI18n();
const router = useRouter();
const route = useRoute();
const keyword = ref<string>('');
const props = defineProps<{
caseId: string; // id
}>();
const columns: MsTableColumn = [
{
title: 'caseManagement.featureCase.defectID',
dataIndex: 'id',
title: 'ID',
dataIndex: 'testPlanNum',
slotName: 'testPlanNum',
showTooltip: true,
width: 90,
},
{
title: 'caseManagement.featureCase.testPlanName',
slotName: 'name',
dataIndex: 'name',
slotName: 'testPlanName',
dataIndex: 'testPlanName',
width: 200,
},
{
@ -62,28 +163,114 @@
title: 'caseManagement.featureCase.planStatus',
slotName: 'planStatus',
dataIndex: 'planStatus',
titleSlotName: 'statusFilter',
width: 200,
},
{
title: 'caseManagement.featureCase.tableColumnExecutionResult',
slotName: 'executionResult',
dataIndex: 'executionResult',
slotName: 'lastExecResult',
dataIndex: 'lastExecResult',
titleSlotName: 'lastExecResultFilter',
width: 200,
},
{
title: 'caseManagement.featureCase.executionTime',
slotName: 'executionTime',
dataIndex: 'executionTime',
slotName: 'lastExecTime',
dataIndex: 'lastExecTime',
width: 200,
showInTable: false,
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
showDrag: true,
},
];
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(getRecycleListRequest, {
columns,
tableKey: TableKeyEnum.CASE_MANAGEMENT_TAB_TEST_PLAN,
scroll: { x: '100%' },
heightUsed: 340,
enableDrag: true,
const statusFilterVisible = ref(false);
const statusFilters = ref<string[]>([]);
const lastExecResultVisible = ref(false);
const lastExecResultFilters = ref<string[]>([]);
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(
getLinkedCaseTestPlanList,
{
columns,
tableKey: TableKeyEnum.CASE_MANAGEMENT_TAB_TEST_PLAN,
scroll: { x: '100%' },
heightUsed: 340,
enableDrag: false,
},
(item) => {
return {
...item,
lastExecTime: item.lastExecTime ? `${dayjs(item.lastExecTime).format('YYYY-MM-DD HH:mm:ss')}` : '',
};
}
);
async function initData() {
setLoadListParams({
keyword: keyword.value,
caseId: props.caseId,
filter: {
planStatus: statusFilters.value,
lastExecResult: lastExecResultFilters.value,
},
});
await loadList();
}
const searchList = debounce(() => {
initData();
}, 100);
function handleFilterHidden(val: boolean) {
if (!val) {
statusFilterVisible.value = false;
lastExecResultVisible.value = false;
searchList();
}
}
function resetStatusFilter() {
statusFilterVisible.value = false;
statusFilters.value = [];
searchList();
}
function resetLastExecuteResultFilter() {
lastExecResultVisible.value = false;
lastExecResultFilters.value = [];
searchList();
}
//
function goToPlan(record: AssociateFunctionalCaseItem) {
router.push({
name: TestPlanRouteEnum.TEST_PLAN_INDEX,
query: {
...route.query,
id: record.testPlanId,
},
state: {
params: JSON.stringify(setLoadListParams()),
},
});
}
watch(
() => props.caseId,
(val) => {
if (val) {
initData();
}
}
);
onMounted(() => {
initData();
});
</script>

View File

@ -65,12 +65,12 @@ export const executionResultMap: Record<string, any> = {
statusText: t('caseManagement.featureCase.passed'),
color: '',
},
SKIPPED: {
/* SKIPPED: {
key: 'SKIPPED',
icon: StatusType.SKIPPED,
statusText: t('caseManagement.featureCase.skip'),
color: 'text-[rgb(var(--link-6))]',
},
}, */
BLOCKED: {
key: 'BLOCKED',
icon: StatusType.BLOCKED,