feat(用例管理): 用例详情新增测试计划tab页
This commit is contained in:
parent
4f97fd5428
commit
7478a1dff0
|
@ -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 = "计划状态")
|
||||
|
|
|
@ -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>
|
|
@ -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 {};
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue