feat(测试计划): 测试计划table联调和细节完善以及测试计划用例详情缺陷管理列表tab

This commit is contained in:
xinxin.wu 2024-05-14 17:19:01 +08:00 committed by Craftsman
parent d4b86ac339
commit 22e9354a51
25 changed files with 1092 additions and 309 deletions

View File

@ -3,13 +3,17 @@ import {
addTestPlanModuleUrl,
AddTestPlanUrl,
archivedPlanUrl,
associationCaseToPlanUrl,
batchArchivedPlanUrl,
batchCopyPlanUrl,
batchDeletePlanUrl,
BatchDisassociateCaseUrl,
batchMovePlanUrl,
copyTestPlanUrl,
deletePlanUrl,
DeleteTestPlanModuleUrl,
DisassociateCaseUrl,
followPlanUrl,
GetFeatureCaseModuleCountUrl,
GetFeatureCaseModuleUrl,
GetPlanDetailFeatureCaseListUrl,
@ -20,6 +24,7 @@ import {
GetTestPlanModuleUrl,
MoveTestPlanModuleUrl,
planDetailBugPageUrl,
planPassRateUrl,
updateTestPlanModuleUrl,
UpdateTestPlanUrl,
} from '@/api/requrls/test-plan/testPlan';
@ -29,8 +34,11 @@ import type { CommonList, MoveModules, TableQueryParams } from '@/models/common'
import { ModuleTreeNode } from '@/models/common';
import type {
AddTestPlanParams,
AssociateCaseRequestType,
BatchFeatureCaseParams,
DisassociateCaseParams,
FollowPlanParams,
PassRateCountDetail,
PlanDetailBugItem,
PlanDetailFeatureCaseItem,
PlanDetailFeatureCaseListQueryParams,
@ -78,6 +86,10 @@ export function getTestPlanList(data: TableQueryParams) {
export function addTestPlan(data: AddTestPlanParams) {
return MSR.post({ url: AddTestPlanUrl, data });
}
// 创建测试计划
export function copyTestPlan(data: AddTestPlanParams) {
return MSR.post({ url: copyTestPlanUrl, data });
}
// 获取测试计划详情
export function getTestPlanDetail(id: string) {
@ -112,10 +124,26 @@ export function batchCopyPlan(data: TableQueryParams) {
export function batchMovePlan(data: TableQueryParams) {
return MSR.post({ url: batchMovePlanUrl, data });
}
// 批量移动测试计划
export function batchArchivedPlan(data: TableQueryParams) {
return MSR.post({ url: batchArchivedPlanUrl, data });
}
// 计划详情缺陷管理列表
export function planDetailBugPage(data: TableQueryParams) {
return MSR.post<CommonList<PlanDetailBugItem>>({ url: planDetailBugPageUrl, data });
}
// 关注
export function followPlanRequest(data: FollowPlanParams) {
return MSR.post({ url: followPlanUrl, data });
}
// 关联用例到测试计划
export function associationCaseToPlan(data: AssociateCaseRequestType) {
return MSR.post({ url: associationCaseToPlanUrl, data });
}
// 测试计划通过率执行进度
export function getPlanPassRate(data: (string | undefined)[]) {
return MSR.post<PassRateCountDetail[]>({ url: planPassRateUrl, data });
}
// 计划详情-功能用例列表
export function getPlanDetailFeatureCaseList(data: PlanDetailFeatureCaseListQueryParams) {
return MSR.post<CommonList<PlanDetailFeatureCaseItem>>({ url: GetPlanDetailFeatureCaseListUrl, data });

View File

@ -27,11 +27,21 @@ export const getStatisticalCountUrl = '/test-plan/getCount';
// 归档
export const archivedPlanUrl = '/test-plan/archived';
// 批量复制
export const batchCopyPlanUrl = '/test-plan/batch/copy';
export const batchCopyPlanUrl = '/test-plan/batch-copy';
// 批量移动
export const batchMovePlanUrl = '/test-plan/batch/move';
// 批量移动
export const batchArchivedPlanUrl = '/test-plan/batch-archived';
// 计划详情缺陷管理列表
export const planDetailBugPageUrl = '/test-plan/bug/page';
// 关注测试计划
export const followPlanUrl = '/test-plan/edit/follower';
// 复制测试计划
export const copyTestPlanUrl = '/test-plan/copy';
// 关联测试计划
export const associationCaseToPlanUrl = '/test-plan/association';
// 测试计划通过率执行进度
export const planPassRateUrl = '/test-plan/statistics';
// 计划详情-功能用例列表
export const GetPlanDetailFeatureCaseListUrl = '/test-plan/functional/case/page';
// 计划详情-功能用例-获取模块数量

View File

@ -1,4 +1,4 @@
import type { planStatusType, TestPlanDetail } from '@/models/testPlan/testPlan';
import type { PassRateCountDetail, planStatusType, TestPlanDetail } from '@/models/testPlan/testPlan';
// TODO: 对照后端字段
// 测试计划详情
@ -23,4 +23,20 @@ export const testPlanDefaultDetail: TestPlanDetail = {
underReviewedCount: 0,
};
export const initDetailCount: PassRateCountDetail = {
id: '',
passThreshold: 0,
passRate: 0,
executeRate: 0,
successCount: 0,
errorCount: 0,
fakeErrorCount: 0,
blockCount: 0,
pendingCount: 0,
caseTotal: 0,
functionalCaseCount: 0,
apiCaseCount: 0,
apiScenarioCount: 0,
};
export default {};

View File

@ -38,6 +38,7 @@ export default {
'common.editFailed': 'Edit failed',
'common.saveSuccess': 'Save success',
'common.saveFailed': 'Save failed',
'common.associated': 'Associated',
'common.linkSuccess': 'Link success',
'common.unLinkSuccess': 'Unlink success',
'common.confirmEnable': 'Confirm enable',
@ -168,6 +169,8 @@ export default {
'common.unExecute': 'Not executed',
'common.pass': 'Pass',
'common.unPass': 'Fail pass',
'common.block': 'block',
'common.fakeError': 'Fake error',
'common.belongModule': 'Belong module',
'common.moreSetting': 'More settings',
};

View File

@ -39,6 +39,7 @@ export default {
'common.editFailed': '编辑失败',
'common.saveSuccess': '保存成功',
'common.saveFailed': '保存失败',
'common.associated': '关联',
'common.linkSuccess': '关联成功',
'common.cancelLink': '取消关联',
'common.unLinkSuccess': '取消关联成功',
@ -168,6 +169,8 @@ export default {
'common.unExecute': '未执行',
'common.pass': '通过',
'common.unPass': '不通过',
'common.block': '阻塞',
'common.fakeError': '误报',
'common.belongModule': '所属模块',
'common.moreSetting': '更多设置',
'common.executionResult': '执行结果',

View File

@ -29,8 +29,11 @@ export interface AssociateCaseRequest extends BatchApiParams {
apiCaseSelectIds?: string[];
apiScenarioSelectIds?: string[];
totalCount?: number;
testPlanId?: string;
}
export type AssociateCaseRequestType = Pick<AssociateCaseRequest, 'functionalSelectIds' | 'testPlanId'>;
export interface AddTestPlanParams {
id?: string;
name: string;
@ -45,11 +48,12 @@ export interface AddTestPlanParams {
repeatCase: boolean; // 是否允许重复添加用例
passThreshold: number;
type?: string;
baseAssociateCaseRequest?: AssociateCaseRequest;
baseAssociateCaseRequest?: AssociateCaseRequest | null;
groupOption?: boolean;
cycle?: number[];
projectId?: string;
testPlanId?: string;
functionalCaseCount?: number;
}
// TODO: 对照后端字段
@ -66,6 +70,7 @@ export interface TestPlanDetail extends AddTestPlanParams {
unPassCount: number;
reReviewedCount: number;
underReviewedCount: number;
functionalCaseCount?: number;
}
// 计划分页
@ -160,4 +165,20 @@ export interface BatchFeatureCaseParams extends BatchActionQueryParams {
moduleIds?: string[];
}
export interface PassRateCountDetail {
id: string;
passThreshold: number;
passRate: number;
executeRate: number;
successCount: number;
errorCount: number;
fakeErrorCount: number;
blockCount: number;
pendingCount: number;
caseTotal: number;
functionalCaseCount: number;
apiCaseCount: number;
apiScenarioCount: number;
}
export default {};

View File

@ -273,7 +273,6 @@
getCaseDetail,
getCaseModuleTree,
} from '@/api/modules/case-management/featureCase';
import { postTabletList } from '@/api/modules/project-management/menuManagement';
import { PreviewEditorImageUrl } from '@/api/requrls/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal';
@ -282,7 +281,6 @@
import useUserStore from '@/store/modules/user';
import { characterLimit } from '@/utils';
import { translateTextToPX } from '@/utils/css';
import { hasAnyPermission } from '@/utils/permission';
import type { CustomAttributes, DetailCase, TabItemType } from '@/models/caseManagement/featureCase';
import { ModuleTreeNode } from '@/models/common';

View File

@ -0,0 +1,158 @@
<template>
<ms-base-table ref="bugTableRef" v-bind="linkPropsRes" v-on="linkTableEvent">
<template #num="{ record }">
<span type="text" class="one-line-text cursor-pointer px-0 text-[rgb(var(--primary-5))]">{{ record.num }}</span>
</template>
<template #name="{ record }">
<span class="one-line-text max-w-[150px]"> {{ characterLimit(record.name) }}</span>
<a-popover title="" position="right" style="width: 480px">
<span class="ml-1 text-[rgb(var(--primary-5))]">{{ t('caseManagement.featureCase.preview') }}</span>
<template #content>
<div v-dompurify-html="record.content" class="markdown-body" style="margin-left: 48px"> </div>
</template>
</a-popover>
</template>
<template #severityFilter="{ columnConfig }">
<TableFilter
v-model:visible="severityFilterVisible"
v-model:status-filters="severityFilterValue"
:title="(columnConfig.title as string)"
:list="severityFilterOptions"
value-key="value"
@search="searchData()"
>
<template #item="{ item }">
{{ item.text }}
</template>
</TableFilter>
</template>
<template #statusName="{ record }">
<div class="one-line-text">{{ record.statusName || '-' }}</div>
</template>
<template #handleUserName="{ record }">
<a-tooltip :content="record.handleUserName">
<div class="one-line-text max-w-[200px]">{{ characterLimit(record.handleUserName) || '-' }}</div>
</a-tooltip>
</template>
<template #operation="{ record }">
<MsButton v-permission="['FUNCTIONAL_CASE:READ+UPDATE']" @click="cancelLink(record.id)">{{
t('caseManagement.featureCase.cancelLink')
}}</MsButton>
</template>
<template v-if="(keyword || '').trim() === ''" #empty>
<div class="flex w-full items-center justify-center text-[var(--color-text-4)]">
{{ t('caseManagement.featureCase.tableNoDataWidthComma') }}
<span v-if="hasAnyPermission(['FUNCTIONAL_CASE:READ+UPDATE', 'PROJECT_BUG:READ+ADD'])">{{
t('caseManagement.featureCase.please')
}}</span>
<MsButton
v-if="hasAnyPermission(['FUNCTIONAL_CASE:READ+UPDATE'])"
:disabled="!props.bugTotal"
class="ml-[8px]"
@click="linkDefect"
>
{{ t('caseManagement.featureCase.linkDefect') }}
</MsButton>
<span v-if="hasAnyPermission(['PROJECT_BUG:READ+ADD'])">{{ t('caseManagement.featureCase.or') }}</span>
<MsButton v-permission="['PROJECT_BUG:READ+ADD']" class="ml-[8px]" @click="createDefect">
{{ t('caseManagement.featureCase.createDefect') }}
</MsButton>
</div>
</template>
</ms-base-table>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import MsButton from '@/components/pure/ms-button/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 TableFilter from '@/views/case-management/caseManagementFeature/components/tableFilter.vue';
import { getLinkedCaseBugList } from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store';
import { characterLimit } from '@/utils';
import { hasAnyPermission } from '@/utils/permission';
import { BugOptionItem } from '@/models/bug-management';
const appStore = useAppStore();
const { t } = useI18n();
const props = defineProps<{
caseId: string;
keyword: string;
bugColumns: MsTableColumn;
bugTotal: number; //
}>();
const emit = defineEmits<{
(e: 'link'): void;
(e: 'new'): void;
(e: 'cancelLink', bugId: string): void;
}>();
const severityFilterVisible = ref(false);
const severityFilterOptions = ref<BugOptionItem[]>([]);
const bugTableRef = ref();
const {
propsRes: linkPropsRes,
propsEvent: linkTableEvent,
loadList: loadLinkList,
setLoadListParams: setLinkListParams,
} = useTable(getLinkedCaseBugList, {
columns: props.bugColumns,
scroll: { x: 'auto' },
heightUsed: 340,
enableDrag: false,
});
const severityColumnId = ref('');
const severityFilterValue = ref<string[]>([]);
function initTableParams() {
const filterParams: Record<string, any> = {
// status: statusFilterValue.value,
// handleUser: handleUserFilterValue.value,
};
// TODO
filterParams[severityColumnId.value] = severityFilterValue.value;
return {
keyword: props.keyword,
caseId: props.caseId,
projectId: appStore.currentProjectId,
condition: {
keyword: props.keyword,
filter: linkPropsRes.value.filter,
},
};
}
function searchData() {
setLinkListParams(initTableParams());
loadLinkList();
}
function linkDefect() {
emit('link');
}
function createDefect() {
emit('new');
}
function cancelLink(id: string) {
emit('cancelLink', id);
}
onBeforeMount(() => {
searchData();
});
defineExpose({
searchData,
bugTableRef,
});
</script>
<style scoped></style>

View File

@ -76,7 +76,7 @@
{
title: 'caseManagement.featureCase.tableColumnID',
dataIndex: 'num',
width: 200,
width: 150,
showInTable: true,
showTooltip: true,
ellipsis: true,
@ -88,7 +88,7 @@
dataIndex: 'name',
showInTable: true,
showTooltip: true,
width: 300,
width: 200,
ellipsis: true,
showDrag: false,
},
@ -99,7 +99,7 @@
dataIndex: 'statusName',
showInTable: true,
showTooltip: true,
width: 300,
width: 200,
ellipsis: true,
showDrag: false,
},
@ -109,6 +109,7 @@
dataIndex: 'tags',
showInTable: true,
isTag: true,
width: 300,
showDrag: true,
},
{
@ -117,7 +118,7 @@
dataIndex: 'handleUserName',
showInTable: true,
showTooltip: true,
width: 300,
width: 200,
ellipsis: true,
showDrag: false,
},
@ -127,7 +128,7 @@
dataIndex: 'createUser',
showInTable: true,
showTooltip: true,
width: 300,
width: 200,
ellipsis: true,
},
{
@ -146,7 +147,6 @@
columns,
tableKey: TableKeyEnum.CASE_MANAGEMENT_TAB_DEFECT,
selectable: true,
scroll: { x: 'auto' },
heightUsed: 340,
enableDrag: false,
},

View File

@ -56,65 +56,17 @@
></a-input-search>
</div>
</div>
<ms-base-table v-if="showType === 'link'" ref="bugTableRef" v-bind="linkPropsRes" v-on="linkTableEvent">
<template #name="{ record }">
<span class="one-line-text max-w-[150px]"> {{ characterLimit(record.name) }}</span>
<a-popover title="" position="right" style="width: 480px">
<span class="ml-1 text-[rgb(var(--primary-5))]">{{ t('caseManagement.featureCase.preview') }}</span>
<template #content>
<div v-dompurify-html="record.content" class="markdown-body" style="margin-left: 48px"> </div>
</template>
</a-popover>
</template>
<template #severityFilter="{ columnConfig }">
<TableFilter
v-model:visible="severityFilterVisible"
v-model:status-filters="severityFilterValue"
:title="(columnConfig.title as string)"
:list="severityFilterOptions"
value-key="value"
@search="searchData()"
>
<template #item="{ item }">
{{ item.text }}
</template>
</TableFilter>
</template>
<template #statusName="{ record }">
<div class="one-line-text">{{ record.statusName }}</div>
</template>
<template #handleUserName="{ record }">
<a-tooltip :content="record.handleUserName">
<div class="one-line-text max-w-[200px]">{{ characterLimit(record.handleUserName) }}</div>
</a-tooltip>
</template>
<template #operation="{ record }">
<MsButton v-permission="['FUNCTIONAL_CASE:READ+UPDATE']" @click="cancelLink(record.id)">{{
t('caseManagement.featureCase.cancelLink')
}}</MsButton>
</template>
<template v-if="(keyword || '').trim() === ''" #empty>
<div class="flex w-full items-center justify-center text-[var(--color-text-4)]">
{{ t('caseManagement.featureCase.tableNoDataWidthComma') }}
<span v-if="hasAnyPermission(['FUNCTIONAL_CASE:READ+UPDATE', 'PROJECT_BUG:READ+ADD'])">{{
t('caseManagement.featureCase.please')
}}</span>
<MsButton
v-if="hasAnyPermission(['FUNCTIONAL_CASE:READ+UPDATE'])"
:disabled="!total"
class="ml-[8px]"
@click="linkDefect"
>
{{ t('caseManagement.featureCase.linkDefect') }}
</MsButton>
<span v-if="hasAnyPermission(['PROJECT_BUG:READ+ADD'])">{{ t('caseManagement.featureCase.or') }}</span>
<MsButton v-permission="['PROJECT_BUG:READ+ADD']" class="ml-[8px]" @click="createDefect">
{{ t('caseManagement.featureCase.createDefect') }}
</MsButton>
</div>
</template>
</ms-base-table>
<BugList
v-if="showType === 'link'"
ref="bugTableListRef"
:case-id="props.caseId"
:keyword="keyword"
:bug-total="total"
:bug-columns="columns"
@link="linkDefect"
@new="createDefect"
@cancel-link="cancelLink"
/>
<ms-base-table v-else v-bind="testPlanPropsRes" ref="planTableRef" v-on="testPlanTableEvent">
<template #name="{ record }">
<div class="one-line-text max-w-[300px]"> {{ record.name }}</div>
@ -143,7 +95,7 @@
:title="(columnConfig.title as string)"
:list="severityFilterOptions"
value-key="value"
@search="searchData()"
@search="getFetch()"
>
<template #item="{ item }">
{{ item.text }}
@ -182,6 +134,7 @@
import type { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import AddDefectDrawer from './addDefectDrawer.vue';
import BugList from './bugList.vue';
import LinkDefectDrawer from './linkDefectDrawer.vue';
import TableFilter from '@/views/case-management/caseManagementFeature/components/tableFilter.vue';
@ -200,7 +153,8 @@
import { BugListItem, BugOptionItem } from '@/models/bug-management';
import type { TableQueryParams } from '@/models/common';
import { TestPlanRouteEnum } from '@/enums/routeEnum';
import { FilterSlotNameEnum } from '@/enums/tableFilterEnum';
import { makeColumns } from '@/views/case-management/caseManagementFeature/components/utils';
const featureCaseStore = useFeatureCaseStore();
@ -210,7 +164,7 @@
const props = defineProps<{
caseId: string;
}>();
// const activeTab = computed(() => featureCaseStore.activeTab);
const showType = ref('link');
const keyword = ref<string>('');
@ -291,17 +245,6 @@
showDrag: false,
},
];
const {
propsRes: linkPropsRes,
propsEvent: linkTableEvent,
loadList: loadLinkList,
setLoadListParams: setLinkListParams,
} = useTable(getLinkedCaseBugList, {
columns,
scroll: { x: 'auto' },
heightUsed: 340,
enableDrag: false,
});
const testPlanColumns: MsTableColumn = [
{
@ -378,58 +321,31 @@
filterParams[severityColumnId.value] = severityFilterValue.value;
return {
keyword: keyword.value,
caseId: showType.value === 'link' ? props.caseId : null,
testPlanCaseId: showType.value === 'link' ? null : props.caseId,
testPlanCaseId: props.caseId,
projectId: appStore.currentProjectId,
condition: {
keyword: keyword.value,
filter: showType.value === 'link' ? linkPropsRes : 'testPlanPropsRes',
filter: testPlanPropsRes.value.filter,
},
};
}
function searchData() {
if (showType.value === 'link') {
setLinkListParams(initTableParams());
loadLinkList();
} else {
setTestPlanListParams(initTableParams());
testPlanLinkList();
}
}
const bugTableRef = ref();
const bugTableListRef = ref();
const planTableRef = ref();
function makeColumns(columnData: MsTableColumn) {
const optionsMap: Record<string, any> = {
status: statusFilterOptions.value,
handleUser: handleUserFilterOptions.value,
};
return columnData.map((e) => {
if (Object.prototype.hasOwnProperty.call(optionsMap, e.dataIndex as string)) {
return {
...e,
filterConfig: {
...e.filterConfig,
options: optionsMap[e.dataIndex as string],
},
};
}
return { ...e };
});
}
async function initFilterOptions() {
if (hasAnyPermission(['PROJECT_BUG:READ'])) {
const res = await getCustomOptionHeader(appStore.currentProjectId);
handleUserFilterOptions.value = res.handleUserOption;
statusFilterOptions.value = res.statusOption;
const optionsMap: Record<string, any> = {
status: statusFilterOptions.value,
handleUser: handleUserFilterOptions.value,
};
if (showType.value === 'link') {
const columnList = makeColumns(columns);
bugTableRef.value.initColumn(columnList);
const columnList = makeColumns(optionsMap, columns);
bugTableListRef.value.bugTableRef.initColumn(columnList);
} else {
const planColumnList = makeColumns(testPlanColumns);
const planColumnList = makeColumns(optionsMap, testPlanColumns);
planTableRef.value.initColumn(planColumnList);
}
}
@ -440,31 +356,21 @@
return;
}
if (showType.value === 'link') {
setLinkListParams({ keyword: keyword.value, projectId: appStore.currentProjectId, caseId: props.caseId });
await loadLinkList();
const { msPagination } = linkPropsRes.value;
featureCaseStore.setListCount(featureCaseStore.activeTab, msPagination?.total || 0);
bugTableListRef.value?.searchData();
} else {
setTestPlanListParams({
keyword: keyword.value,
projectId: appStore.currentProjectId,
testPlanCaseId: props.caseId,
});
setTestPlanListParams(initTableParams());
await testPlanLinkList();
featureCaseStore.getCaseCounts(props.caseId);
}
featureCaseStore.getCaseCounts(props.caseId);
}
async function resetFetch() {
if (showType.value === 'link') {
setLinkListParams({ keyword: '', projectId: appStore.currentProjectId, caseId: props.caseId });
await loadLinkList();
const { msPagination } = linkPropsRes.value;
featureCaseStore.setListCount(featureCaseStore.activeTab, msPagination?.total || 0);
bugTableListRef.value?.searchData();
} else {
setTestPlanListParams({ keyword: '', projectId: appStore.currentProjectId, testPlanCaseId: props.caseId });
await testPlanLinkList();
featureCaseStore.getCaseCounts(props.caseId);
}
featureCaseStore.getCaseCounts(props.caseId);
}
const cancelLoading = ref<boolean>(false);
//
@ -473,8 +379,8 @@
try {
if (showType.value === 'link') {
await cancelAssociatedDebug(id);
getFetch();
Message.success(t('caseManagement.featureCase.cancelLinkSuccess'));
getFetch();
}
} catch (error) {
console.log(error);
@ -566,13 +472,10 @@
}
);
onMounted(() => {
getFetch();
initBugList();
});
onBeforeMount(() => {
initFilterOptions();
getFetch();
initBugList();
});
</script>

View File

@ -59,7 +59,6 @@
import { ReviewCaseItem, ReviewStatus } from '@/models/caseManagement/caseReview';
import { CaseManagementRouteEnum } from '@/enums/routeEnum';
import { TableKeyEnum } from '@/enums/tableEnum';
import { FilterSlotNameEnum } from '@/enums/tableFilterEnum';
import { statusIconMap } from '../utils';

View File

@ -1,4 +1,5 @@
import type { FormItem } from '@/components/pure/ms-form-create/types';
import type { MsTableColumn } from '@/components/pure/ms-table/type';
import { MsTableColumnData } from '@/components/pure/ms-table/type';
import { getFileEnum } from '@/components/pure/ms-upload/iconMap';
import type { MsFileItem } from '@/components/pure/ms-upload/types';
@ -253,3 +254,22 @@ export function initFormCreate(customFields: CustomAttributes[], permission: str
};
}) as FormItem[];
}
export function makeColumns(optionsMap: Record<string, any>, columnData: MsTableColumn) {
// const optionsMap: Record<string, any> = {
// status: statusFilterOptions.value,
// handleUser: handleUserFilterOptions.value,
// };
return columnData.map((e) => {
if (Object.prototype.hasOwnProperty.call(optionsMap, e.dataIndex as string)) {
return {
...e,
filterConfig: {
...e.filterConfig,
options: optionsMap[e.dataIndex as string],
},
};
}
return { ...e };
});
}

View File

@ -250,7 +250,7 @@
return Object.keys(reviewStatusMap).map((key) => {
return {
value: key,
label: reviewStatusMap[key as ReviewStatus].label,
label: t(reviewStatusMap[key as ReviewStatus].label),
};
});
});

View File

@ -48,19 +48,19 @@
import { useI18n } from '@/hooks/useI18n';
import { characterLimit } from '@/utils';
import type { TestPlanItem } from '@/models/testPlan/testPlan';
import type { TestPlanDetail, TestPlanItem } from '@/models/testPlan/testPlan';
const { t } = useI18n();
const props = defineProps<{
visible: boolean;
// isScheduled: boolean; // TODO
record: TestPlanItem | undefined; // record
record: TestPlanItem | TestPlanDetail | undefined; // record
}>();
const emit = defineEmits<{
(e: 'update:visible', val: boolean): void;
(e: 'success'): void;
(e: 'success', isDelete: boolean): void;
}>();
const showModalVisible = useVModel(props, 'visible', emit);
@ -76,11 +76,13 @@
confirmLoading.value = true;
if (isDelete) {
await deletePlan(props.record?.id);
emit('success', true);
} else {
await archivedPlan(props.record?.id);
emit('success', false);
}
Message.success(isDelete ? t('common.deleteSuccess') : t('common.batchArchiveSuccess'));
emit('success');
showModalVisible.value = false;
} catch (error) {
console.log(error);
} finally {

View File

@ -5,12 +5,11 @@
:get-modules-func="getCaseModuleTree"
:get-table-func="getCaseList"
:confirm-loading="confirmLoading"
:associated-ids="[]"
:associated-ids="props.hasNotAssociatedIds || []"
:project-id="currentProjectId"
:type="RequestModuleEnum.CASE_MANAGEMENT"
hide-project-select
is-hidden-case-level
:has-not-associated-ids="props.hasNotAssociatedIds"
@save="saveHandler"
>
</MsCaseAssociate>

View File

@ -102,15 +102,22 @@
<template #status="{ record }">
<MsStatusTag :status="record.status" />
</template>
<template #moduleId="{ record }">
<a-tooltip :content="getModules(record.moduleId, props.moduleTree)" position="top">
<span class="one-line-text inline-block">
{{ getModules(record.moduleId, props.moduleTree) }}
</span>
</a-tooltip>
</template>
<!-- <template #passRate="{ record }">
<template #passRate="{ record }">
<div class="mr-[8px] w-[100px]">
<StatusProgress :status-detail="record.statusDetail" height="5px" />
<StatusProgress :status-detail="initDefaultCountDetailMap[record.id]" height="5px" />
</div>
<div class="text-[var(--color-text-1)]">
{{ `${record.passRate || 0}%` }}
</div>
</template> -->
</template>
<template #passRateTitleSlot="{ columnConfig }">
<div class="flex items-center text-[var(--color-text-3)]">
{{ t(columnConfig.title as string) }}
@ -122,17 +129,17 @@
</a-tooltip>
</div>
</template>
<!-- <template #useCount="{ record }">
<a-popover position="bottom" content-class="p-[16px]" trigger="click">
<div>{{ record.useCaseCount.caseCount }}</div>
<template #functionalCaseCount="{ record }">
<a-popover position="bottom" content-class="p-[16px]" :disabled="record.functionalCaseCount < 1">
<div>{{ record.functionalCaseCount }}</div>
<template #content>
<table class="min-w-[144px]">
<table class="min-w-[140px] max-w-[176px]">
<tr>
<td class="popover-label-td">
<div>{{ t('testPlan.testPlanIndex.TotalCases') }}</div>
</td>
<td class="popover-value-td">
{{ record.useCaseCount.caseCount }}
{{ record.caseTotal }}
</td>
</tr>
<tr>
@ -140,7 +147,7 @@
<div class="text-[var(--color-text-1)]">{{ t('testPlan.testPlanIndex.functionalUseCase') }}</div>
</td>
<td class="popover-value-td">
{{ record.useCaseCount.caseCount }}
{{ record.functionalCaseCount }}
</td>
</tr>
<tr>
@ -148,7 +155,7 @@
<div class="text-[var(--color-text-1)]">{{ t('testPlan.testPlanIndex.apiCase') }}</div>
</td>
<td class="popover-value-td">
{{ record.useCaseCount.caseCount }}
{{ record.apiCaseCount }}
</td>
</tr>
<tr>
@ -156,25 +163,41 @@
<div class="text-[var(--color-text-1)]">{{ t('testPlan.testPlanIndex.apiScenarioCase') }}</div>
</td>
<td class="popover-value-td">
{{ record.useCaseCount.caseCount }}
{{ record.apiScenarioCount }}
</td>
</tr>
</table>
</template>
</a-popover>
</template> -->
</template>
<template #operation="{ record }">
<div class="flex items-center">
<MsButton class="!mx-0">{{ t('testPlan.testPlanIndex.execution') }}</MsButton>
<a-divider direction="vertical" :margin="8"></a-divider>
<MsButton v-permission="['PROJECT_TEST_PLAN:READ+UPDATE']" class="!mx-0" @click="emit('edit', record.id)">{{
t('common.edit')
<MsButton v-if="record.functionalCaseCount > 0" class="!mx-0">{{
t('testPlan.testPlanIndex.execution')
}}</MsButton>
<a-divider direction="vertical" :margin="8"></a-divider>
<a-divider v-if="record.functionalCaseCount > 0" direction="vertical" :margin="8"></a-divider>
<MsTableMoreAction :list="moreActions" @select="handleMoreActionSelect($event, record)" />
<MsButton
v-permission="['PROJECT_TEST_PLAN:READ+UPDATE']"
class="!mx-0"
@click="emit('editOrCopy', record.id, false)"
>{{ t('common.edit') }}</MsButton
>
<a-divider direction="vertical" :margin="8"></a-divider>
<MsButton
v-if="record.functionalCaseCount < 1"
v-permission="['PROJECT_TEST_PLAN:READ+UPDATE']"
class="!mx-0"
@click="emit('editOrCopy', record.id, true)"
>{{ t('common.copy') }}</MsButton
>
<a-divider v-if="record.functionalCaseCount < 1" direction="vertical" :margin="8"></a-divider>
<MsTableMoreAction
:list="getMoreActions(record.status, record.functionalCaseCount)"
@select="handleMoreActionSelect($event, record)"
/>
</div>
</template>
</MsBaseTable>
@ -250,9 +273,11 @@
import {
archivedPlan,
batchArchivedPlan,
batchCopyPlan,
batchDeletePlan,
batchMovePlan,
getPlanPassRate,
getTestPlanDetail,
getTestPlanList,
getTestPlanModule,
@ -264,13 +289,15 @@
import { characterLimit } from '@/utils';
import { hasAnyPermission } from '@/utils/permission';
import type { planStatusType, TestPlanItem } from '@/models/testPlan/testPlan';
import { ModuleTreeNode } from '@/models/common';
import type { PassRateCountDetail, planStatusType, TestPlanItem } from '@/models/testPlan/testPlan';
import { TestPlanRouteEnum } from '@/enums/routeEnum';
import { ColumnEditTypeEnum, TableKeyEnum } from '@/enums/tableEnum';
import { FilterSlotNameEnum } from '@/enums/tableFilterEnum';
import { testPlanTypeEnum } from '@/enums/testPlanEnum';
import { planStatusOptions } from '../config';
import { getModules } from '@/views/case-management/caseManagementFeature/components/utils';
const tableStore = useTableStore();
const appStore = useAppStore();
@ -284,11 +311,12 @@
offspringIds: string[]; // id
modulesCount: Record<string, number>; //
nodeName: string; //
moduleTree: ModuleTreeNode[];
}>();
const emit = defineEmits<{
(e: 'init', params: any): void;
(e: 'edit', id: string): void;
(e: 'editOrCopy', id: string, isCopy: boolean): void;
}>();
const columns: MsTableColumn = [
@ -355,9 +383,8 @@
},
{
title: 'testPlan.testPlanIndex.useCount',
slotName: 'useCount',
dataIndex: 'useCount',
showTooltip: true,
slotName: 'functionalCaseCount',
dataIndex: 'functionalCaseCount',
showInTable: true,
width: 150,
showDrag: true,
@ -373,8 +400,8 @@
},
{
title: 'testPlan.testPlanIndex.belongModule',
slotName: 'moduleName',
dataIndex: 'moduleName',
slotName: 'moduleId',
dataIndex: 'moduleId',
showInTable: true,
showDrag: true,
width: 200,
@ -508,34 +535,45 @@
],
};
const moreActions: ActionsItem[] = [
{
label: 'common.copy',
eventTag: 'copy',
},
// TODO
// {
// label: 'testPlan.testPlanIndex.createScheduledTask',
// eventTag: 'createScheduledTask',
// },
// {
// label: 'testPlan.testPlanIndex.configuration',
// eventTag: 'config',
// },
const archiveActions: ActionsItem[] = [
{
label: 'common.archive',
eventTag: 'archive',
},
];
const copyActions: ActionsItem[] = [
{
isDivider: true,
},
{
label: 'common.delete',
danger: true,
eventTag: 'delete',
label: 'common.copy',
eventTag: 'copy',
},
];
function getMoreActions(status: planStatusType, useCount: number) {
const copyAction = useCount > 0 ? copyActions : [];
if (status === 'COMPLETED' || status === 'ARCHIVED') {
return [
...copyAction,
{
isDivider: true,
},
{
label: 'common.delete',
danger: true,
eventTag: 'delete',
},
];
}
return [
...copyAction,
...archiveActions,
{
label: 'common.delete',
danger: true,
eventTag: 'delete',
},
];
}
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(
getTestPlanList,
{
@ -607,6 +645,19 @@
});
}
const initDefaultCountDetailMap = ref<Record<string, PassRateCountDetail>>({});
async function getStatistics(selectedPlanIds: (string | undefined)[]) {
try {
const result = await getPlanPassRate(selectedPlanIds);
result.forEach((item: PassRateCountDetail) => {
initDefaultCountDetailMap.value[item.id] = item;
});
} catch (error) {
console.log(error);
}
}
async function fetchData() {
resetSelector();
await loadPlanList();
@ -711,7 +762,18 @@
},
onBeforeOk: async () => {
try {
const { selectedIds, selectAll, excludeIds } = batchParams.value;
await batchArchivedPlan({
selectIds: batchParams.value.selectedIds || [],
condition: {
keyword: keyword.value,
filter: propsRes.value.filter,
combine: batchParams.value.condition,
},
projectId: appStore.currentProjectId,
moduleIds: props.activeFolder === 'all' ? [] : [props.activeFolder, ...props.offspringIds],
type: showType.value,
moduleId: props.activeFolder,
});
Message.success(t('common.batchArchiveSuccess'));
fetchData();
} catch (error) {
@ -811,9 +873,9 @@
}
}
function deletePlan(record: TestPlanItem) {}
function copyHandler(record: TestPlanItem) {}
function copyHandler(record: TestPlanItem) {
emit('editOrCopy', record.id as string, true);
}
const showScheduledTaskModal = ref<boolean>(false);
function handleScheduledTask() {
@ -821,7 +883,7 @@
}
const showStatusDeleteModal = ref<boolean>(false);
const activeRecord = ref<TestPlanItem>();
const activeRecord = ref<TestPlanItem | undefined>();
function deleteStatusHandler(record: TestPlanItem) {
activeRecord.value = cloneDeep(record);
showStatusDeleteModal.value = true;
@ -884,19 +946,6 @@
// return expandedKeys.value.includes(record.id) ? 'text-[rgb(var(--primary-5))]' : 'text-[var(--color-text-4)]';
// }
function handleFilterHidden(val: boolean) {
if (!val) {
statusFilterVisible.value = false;
fetchData();
}
}
function resetStatusFilter() {
statusFilterVisible.value = false;
statusFilters.value = [];
fetchData();
}
/** *
* 高级检索
*/
@ -926,6 +975,25 @@
fetchData();
});
const planData = computed(() => {
return propsRes.value.data;
});
watch(
() => planData.value,
(val) => {
if (val) {
const selectedPlanIds: (string | undefined)[] = propsRes.value.data.map((e) => e.id) || [];
if (selectedPlanIds.length) {
getStatistics(selectedPlanIds);
}
}
},
{
immediate: true,
}
);
defineExpose({
fetchData,
emitTableParams,

View File

@ -4,18 +4,50 @@
<table class="min-w-[144px]">
<tr>
<td class="popover-label-td">
<div>{{ t('testPlan.testPlanIndex.tolerance') }}</div>
</td>
<td class="popover-value-td">
{{ props.statusDetail.tolerance }}
<div>{{ t('testPlan.testPlanIndex.threshold') }}</div>
</td>
<td class="popover-value-td"> {{ detailCount.passThreshold }}% </td>
</tr>
<tr>
<td class="popover-label-td">
<div>{{ t('testPlan.testPlanIndex.executionProgress') }}</div>
</td>
<td class="popover-value-td"> {{ detailCount.executeRate }}% </td>
</tr>
<tr>
<td class="popover-label-td">
<div class="mb-[2px] mr-[4px] h-[6px] w-[6px] rounded-full bg-[rgb(var(--success-6))]"></div>
<div>{{ t('common.success') }}</div>
</td>
<td class="popover-value-td">
{{ props.statusDetail.executionProgress }}
{{ detailCount.successCount }}
</td>
</tr>
<tr>
<td class="popover-label-td">
<div class="mb-[2px] mr-[4px] h-[6px] w-[6px] rounded-full bg-[rgb(var(--danger-6))]"></div>
<div>{{ t('common.fail') }}</div>
</td>
<td class="popover-value-td">
{{ detailCount.errorCount }}
</td>
</tr>
<tr>
<td class="popover-label-td">
<div class="mb-[2px] mr-[4px] h-[6px] w-[6px] rounded-full bg-[rgb(var(--warning-6))]"></div>
<div>{{ t('common.fakeError') }}</div>
</td>
<td class="popover-value-td">
{{ detailCount.fakeErrorCount }}
</td>
</tr>
<tr>
<td class="popover-label-td">
<div class="mb-[2px] mr-[4px] h-[6px] w-[6px] rounded-full bg-[rgb(var(--link-6))]"></div>
<div>{{ t('common.block') }}</div>
</td>
<td class="popover-value-td">
{{ detailCount.blockCount }}
</td>
</tr>
<tr>
@ -24,34 +56,7 @@
<div>{{ t('common.unExecute') }}</div>
</td>
<td class="popover-value-td">
{{ props.statusDetail.UNPENDING }}
</td>
</tr>
<tr>
<td class="popover-label-td">
<div class="mb-[2px] mr-[4px] h-[6px] w-[6px] rounded-full bg-[rgb(var(--link-6))]"></div>
<div>{{ t('common.running') }}</div>
</td>
<td class="popover-value-td">
{{ props.statusDetail.RUNNING }}
</td>
</tr>
<tr>
<td class="popover-label-td">
<div class="mb-[2px] mr-[4px] h-[6px] w-[6px] rounded-full bg-[rgb(var(--success-6))]"></div>
<div>{{ t('common.pass') }}</div>
</td>
<td class="popover-value-td">
{{ props.statusDetail.SUCCESS }}
</td>
</tr>
<tr>
<td class="popover-label-td">
<div class="mb-[2px] mr-[4px] h-[6px] w-[6px] rounded-full bg-[rgb(var(--danger-6))]"></div>
<div>{{ t('common.unPass') }}</div>
</td>
<td class="popover-value-td">
{{ props.statusDetail.ERROR }}
{{ detailCount.pendingCount }}
</td>
</tr>
</table>
@ -64,35 +69,29 @@
import MsColorLine from '@/components/pure/ms-color-line/index.vue';
import { initDetailCount } from '@/config/testPlan';
import { useI18n } from '@/hooks/useI18n';
import type { PassRateCountDetail } from '@/models/testPlan/testPlan';
const props = defineProps<{
statusDetail: {
tolerance: number;
UNPENDING: number;
RUNNING: number;
SUCCESS: number;
ERROR: number;
executionProgress: string;
[key: string]: any;
};
statusDetail: PassRateCountDetail | undefined;
height: string;
radius?: string;
}>();
const { t } = useI18n();
const getCountTotal = computed(() => {
const { UNPENDING, RUNNING, ERROR, SUCCESS } = props.statusDetail;
return UNPENDING + RUNNING + ERROR + SUCCESS;
const detailCount = ref({ ...initDetailCount });
watchEffect(() => {
detailCount.value = {
...initDetailCount,
...props.statusDetail,
};
});
const colorData = computed(() => {
if (
props.statusDetail.UNPENDING === 0 &&
props.statusDetail.RUNNING === 0 &&
props.statusDetail.ERROR === 0 &&
props.statusDetail.SUCCESS === 0
) {
const { caseTotal, successCount, errorCount, fakeErrorCount, blockCount, pendingCount } = detailCount.value;
if (fakeErrorCount === 0 && blockCount === 0 && errorCount === 0 && successCount === 0 && pendingCount === 0) {
return [
{
percentage: 100,
@ -100,21 +99,33 @@
},
];
}
if (detailCount.value.passRate > detailCount.value.passThreshold) {
return [
{
percentage: 100,
color: 'rgb(var(--success-6))',
},
];
}
return [
{
percentage: (props.statusDetail.SUCCESS / getCountTotal.value) * 100,
percentage: (successCount / caseTotal) * 100,
color: 'rgb(var(--success-6))',
},
{
percentage: (props.statusDetail.ERROR / getCountTotal.value) * 100,
percentage: (errorCount / caseTotal) * 100,
color: 'rgb(var(--danger-6))',
},
{
percentage: (props.statusDetail.RUNNING / getCountTotal.value) * 100,
percentage: (blockCount / caseTotal) * 100,
color: 'rgb(var(--link-6))',
},
{
percentage: (props.statusDetail.UNPENDING / getCountTotal.value) * 100,
percentage: (fakeErrorCount / caseTotal) * 100,
color: 'rgb(var(--warning-6))',
},
{
percentage: (pendingCount / caseTotal) * 100,
color: 'var(--color-text-input-border)',
},
];

View File

@ -1,10 +1,10 @@
<template>
<MsDrawer
v-model:visible="innerVisible"
:title="props.planId?.length ? t('case.updateCase') : t('testPlan.testPlanIndex.createTestPlan')"
:title="modelTitle"
:width="800"
unmount-on-close
:ok-text="props.planId?.length ? 'common.update' : 'common.create'"
:ok-text="okText"
:save-continue-text="t('case.saveContinueText')"
:show-continue="!props.planId?.length"
:ok-loading="drawerLoading"
@ -86,9 +86,7 @@
<div class="text-[var(--color-text-2)]">
{{
t('caseManagement.caseReview.selectedCases', {
count: form.baseAssociateCaseRequest?.selectAll
? form.baseAssociateCaseRequest?.totalCount
: form.baseAssociateCaseRequest?.selectIds.length,
count: getSelectedCount,
})
}}
</div>
@ -143,7 +141,7 @@
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 { addTestPlan, copyTestPlan, getTestPlanDetail, updateTestPlan } from '@/api/modules/test-plan/testPlan';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
@ -154,6 +152,7 @@
const props = defineProps<{
planId?: string;
moduleTree?: ModuleTreeNode[];
isCopy: boolean;
}>();
const innerVisible = defineModel<boolean>('visible', {
required: true,
@ -170,6 +169,7 @@
const drawerLoading = ref(false);
const formRef = ref<FormInstance>();
const initForm: AddTestPlanParams = {
groupId: 'NONE',
name: '',
projectId: '',
moduleId: 'root',
@ -218,8 +218,21 @@
if (!errors) {
drawerLoading.value = true;
try {
const {
id,
name,
moduleId,
tags,
description,
testPlanning,
automaticStatusUpdate,
repeatCase,
passThreshold,
groupOption,
} = form.value;
const params: AddTestPlanParams = {
...cloneDeep(form.value),
groupId: 'NONE',
plannedStartTime: form.value.cycle ? form.value.cycle[0] : undefined,
plannedEndTime: form.value.cycle ? form.value.cycle[1] : undefined,
projectId: appStore.currentProjectId,
@ -228,8 +241,31 @@
await addTestPlan(params);
Message.success(t('common.createSuccess'));
} else {
await updateTestPlan(params);
Message.success(t('common.updateSuccess'));
if (props.isCopy) {
const copyParams: AddTestPlanParams = {
id,
groupId: 'NONE',
name,
moduleId,
tags,
description,
testPlanning,
automaticStatusUpdate,
repeatCase,
passThreshold,
baseAssociateCaseRequest: null,
groupOption,
plannedStartTime: form.value.cycle ? form.value.cycle[0] : undefined,
plannedEndTime: form.value.cycle ? form.value.cycle[1] : undefined,
projectId: appStore.currentProjectId,
type: testPlanTypeEnum.TEST_PLAN,
};
await copyTestPlan(copyParams);
} else {
await updateTestPlan(params);
}
Message.success(props.isCopy ? t('common.copySuccess') : t('common.updateSuccess'));
}
emit('loadPlanList');
} catch (error) {
@ -251,6 +287,9 @@
if (props.planId?.length) {
const result = await getTestPlanDetail(props.planId);
form.value = cloneDeep(result);
let copyName = `copy_${result.name}`;
copyName = copyName.length > 255 ? copyName.slice(0, 255) : copyName;
form.value.name = copyName;
form.value.cycle = [result.plannedStartTime as number, result.plannedEndTime as number];
}
} catch (error) {
@ -268,4 +307,27 @@
}
}
);
const modelTitle = computed(() => {
if (props.planId) {
return props.isCopy ? t('testPlan.testPlanIndex.copyTestPlan') : t('testPlan.testPlanIndex.updateTestPlan');
}
return t('testPlan.testPlanIndex.createTestPlan');
});
const okText = computed(() => {
if (props.planId) {
return props.isCopy ? t('common.copy') : t('common.update');
}
return 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

@ -90,8 +90,8 @@
},
{
title: 'testPlan.bugManagement.defectState',
slotName: 'statusName',
dataIndex: 'statusName',
slotName: 'status',
dataIndex: 'status',
showInTable: true,
showTooltip: true,
width: 200,

View File

@ -0,0 +1,271 @@
<template>
<div>
<div class="mb-4 flex items-center justify-between">
<a-dropdown-button
v-if="hasAnyPermission(['PROJECT_TEST_PLAN:READ+UPDATE']) && total"
type="primary"
@click="handleSelect('associated')"
>
{{ t('common.associated') }}
<template #icon>
<icon-down />
</template>
<template #content>
<a-doption value="new" @click="handleSelect('new')">
{{ t('common.newCreate') }}
</a-doption>
</template>
</a-dropdown-button>
<a-dropdown-button
v-if="hasAnyPermission(['PROJECT_TEST_PLAN:READ+UPDATE']) && !total"
type="primary"
@click="handleSelect('new')"
>
{{ t('common.newCreate') }}
<template #icon>
<icon-down />
</template>
<template #content>
<a-popover title="" position="right">
<a-doption value="associated" :disabled="!total" @click="handleSelect('associated')">
{{ t('common.associated') }}
</a-doption>
<template #content>
<div class="flex items-center text-[14px]">
<span class="text-[var(--color-text-4)]">{{ t('testPlan.featureCase.noBugDataTooltip') }}</span>
<MsButton type="text" @click="handleSelect('new')">
{{ t('testPlan.featureCase.noBugDataNewBug') }}
</MsButton>
</div>
</template>
</a-popover>
</template>
</a-dropdown-button>
<a-input-search
v-model:model-value="keyword"
:placeholder="t('caseManagement.featureCase.searchByName')"
allow-clear
class="mx-[8px] w-[240px]"
@search="initData"
@press-enter="initData"
@clear="initData"
/>
</div>
<BugList
ref="bugTableListRef"
:case-id="props.caseId"
:keyword="keyword"
:bug-total="total"
:bug-columns="columns"
@link="linkDefect"
@new="createDefect"
@cancel-link="cancelLink"
/>
<LinkDefectDrawer
v-model:visible="showLinkDrawer"
:case-id="props.caseId"
:drawer-loading="drawerLoading"
@save="saveHandler"
/>
<AddDefectDrawer v-model:visible="showDrawer" :case-id="props.caseId" @success="initData()" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { Message } from '@arco-design/web-vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import type { MsTableColumn } from '@/components/pure/ms-table/type';
import AddDefectDrawer from '@/views/case-management/caseManagementFeature/components/tabContent/tabBug/addDefectDrawer.vue';
import BugList from '@/views/case-management/caseManagementFeature/components/tabContent/tabBug/bugList.vue';
import LinkDefectDrawer from '@/views/case-management/caseManagementFeature/components/tabContent/tabBug/linkDefectDrawer.vue';
import { getBugList, getCustomOptionHeader } from '@/api/modules/bug-management';
import { associatedDrawerDebug, cancelAssociatedDebug } from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store';
import { hasAnyPermission } from '@/utils/permission';
import { BugOptionItem } from '@/models/bug-management';
import type { TableQueryParams } from '@/models/common';
import { makeColumns } from '@/views/case-management/caseManagementFeature/components/utils';
const { t } = useI18n();
const appStore = useAppStore();
const props = defineProps<{
caseId: string;
}>();
const keyword = ref<string>('');
const columns: MsTableColumn = [
{
title: 'caseManagement.featureCase.tableColumnID',
dataIndex: 'num',
width: 200,
showInTable: true,
showTooltip: true,
showDrag: false,
fixed: 'left',
},
{
title: 'caseManagement.featureCase.defectName',
slotName: 'name',
dataIndex: 'name',
showInTable: true,
showTooltip: false,
width: 200,
ellipsis: true,
showDrag: false,
},
{
title: 'caseManagement.featureCase.defectState',
slotName: 'statusName',
dataIndex: 'status',
filterConfig: {
options: [],
labelKey: 'text',
},
showInTable: true,
width: 150,
ellipsis: true,
showDrag: false,
},
{
title: 'caseManagement.featureCase.updateUser',
slotName: 'handleUserName',
dataIndex: 'handleUser',
filterConfig: {
options: [],
labelKey: 'text',
},
showInTable: true,
width: 200,
ellipsis: true,
},
{
title: 'caseManagement.featureCase.defectSource',
slotName: 'source',
dataIndex: 'source',
showInTable: true,
showTooltip: true,
width: 100,
ellipsis: true,
showDrag: false,
},
{
title: 'caseManagement.featureCase.tableColumnActions',
slotName: 'operation',
dataIndex: 'operation',
fixed: 'right',
width: 100,
showInTable: true,
showDrag: false,
},
];
const bugTableListRef = ref();
async function initData() {
if (!hasAnyPermission(['FUNCTIONAL_CASE:READ', 'FUNCTIONAL_CASE:READ+UPDATE', 'FUNCTIONAL_CASE:READ+DELETE'])) {
return;
}
bugTableListRef.value?.searchData();
}
const showLinkDrawer = ref<boolean>(false);
function linkDefect() {
showLinkDrawer.value = true;
}
const showDrawer = ref<boolean>(false);
function createDefect() {
showDrawer.value = true;
}
function handleSelect(value: string | number | Record<string, any> | undefined) {
switch (value) {
case 'associated':
linkDefect();
break;
default:
createDefect();
break;
}
}
const total = ref<number>(0);
async function initBugList() {
if (!hasAnyPermission(['PROJECT_BUG:READ'])) {
return;
}
const res = await getBugList({
current: 1,
pageSize: 10,
sort: {},
filter: {},
keyword: '',
combine: {},
searchMode: 'AND',
projectId: appStore.currentProjectId,
});
total.value = res.total;
}
const cancelLoading = ref<boolean>(false);
//
async function cancelLink(id: string) {
cancelLoading.value = true;
try {
await cancelAssociatedDebug(id);
Message.success(t('caseManagement.featureCase.cancelLinkSuccess'));
initData();
} catch (error) {
console.log(error);
} finally {
cancelLoading.value = false;
}
}
const handleUserFilterOptions = ref<BugOptionItem[]>([]);
const statusFilterOptions = ref<BugOptionItem[]>([]);
async function initFilterOptions() {
if (hasAnyPermission(['PROJECT_BUG:READ'])) {
const res = await getCustomOptionHeader(appStore.currentProjectId);
handleUserFilterOptions.value = res.handleUserOption;
statusFilterOptions.value = res.statusOption;
const optionsMap: Record<string, any> = {
status: statusFilterOptions.value,
handleUser: handleUserFilterOptions.value,
};
const columnList = makeColumns(optionsMap, columns);
bugTableListRef.value.bugTableRef.initColumn(columnList);
}
}
const drawerLoading = ref<boolean>(false);
async function saveHandler(params: TableQueryParams) {
try {
drawerLoading.value = true;
await associatedDrawerDebug(params);
Message.success(t('caseManagement.featureCase.associatedSuccess'));
initData();
showLinkDrawer.value = false;
} catch (error) {
console.log(error);
} finally {
drawerLoading.value = false;
}
}
onBeforeMount(() => {
initFilterOptions();
initData();
initBugList();
});
</script>
<style scoped></style>

View File

@ -60,7 +60,7 @@
<a-spin :loading="caseDetailLoading" class="relative flex flex-1 flex-col p-[16px]">
<div class="flex">
<div class="mr-[24px] flex flex-1 items-center">
<MsStatusTag :status="caseDetail.status" />
<MsStatusTag :status="caseDetail.status || 'PREPARED'" />
<div class="ml-[8px] mr-[2px] font-medium text-[rgb(var(--primary-5))]">[{{ caseDetail.num }}]</div>
<div class="flex-1 overflow-hidden">
<a-tooltip :content="caseDetail.name">
@ -79,6 +79,10 @@
no-content
class="relative border-b"
/>
<div class="tab-content">
<BugList v-if="activeTab === 'defectList'" :case-id="caseDetail.id" />
<ExecutionHistory v-if="activeTab === 'executionHistory'" :case-id="caseDetail.id" />
</div>
</a-spin>
</div>
</MsCard>
@ -93,6 +97,8 @@
import MsTab from '@/components/pure/ms-tab/index.vue';
import ExecuteResult from '@/components/business/ms-case-associate/executeResult.vue';
import MsStatusTag from '@/components/business/ms-status-tag/index.vue';
import BugList from './bug/index.vue';
import ExecutionHistory from '@/views/test-plan/testPlan/detail/featureCase/detail/executionHistory/index.vue';
import { getPlanDetailFeatureCaseList } from '@/api/modules/test-plan/testPlan';
import { useI18n } from '@/hooks/useI18n';
@ -169,6 +175,14 @@
value: 'detail',
label: t('common.detail'),
},
{
value: 'defectList',
label: t('caseManagement.featureCase.defectList'),
},
{
value: 'executionHistory',
label: t('testPlan.featureCase.executionHistory'),
},
]);
onBeforeMount(async () => {
@ -215,4 +229,8 @@
background-color: var(--color-text-n9);
}
}
.tab-content {
.ms-scroll-bar();
@apply py-4;
}
</style>

View File

@ -9,7 +9,7 @@
hide-divider
>
<template #headerLeft>
<MsStatusTag :status="detail.status" />
<MsStatusTag :status="detail.status || 'PREPARED'" />
<a-tooltip :content="`[${detail.num}]${detail.name}`">
<div class="one-line-text ml-[4px] max-w-[360px] gap-[4px] font-medium text-[var(--color-text-1)]">
<span>[{{ detail.num }}]</span>
@ -18,19 +18,35 @@
</a-tooltip>
</template>
<template #headerRight>
<MsButton v-permission="['PROJECT_TEST_PLAN:READ+ASSOCIATION']" type="button" status="default">
<MsButton v-permission="['PROJECT_TEST_PLAN:READ+ASSOCIATION']" type="button" status="default" @click="linkCase">
<MsIcon type="icon-icon_link-record_outlined1" class="mr-[8px]" />
{{ t('ms.case.associate.title') }}
</MsButton>
<MsButton v-permission="['PROJECT_TEST_PLAN:READ+UPDATE']" type="button" status="default">
<MsButton
v-permission="['PROJECT_TEST_PLAN:READ+UPDATE']"
type="button"
status="default"
@click="editorCopyHandler(false)"
>
<MsIcon type="icon-icon_edit_outlined" class="mr-[8px]" />
{{ t('common.edit') }}
</MsButton>
<MsButton v-permission="['PROJECT_TEST_PLAN:READ+ADD']" type="button" status="default">
<MsButton
v-permission="['PROJECT_TEST_PLAN:READ+ADD']"
type="button"
status="default"
@click="editorCopyHandler(true)"
>
<MsIcon type="icon-icon_copy_outlined" class="mr-[8px]" />
{{ t('common.copy') }}
</MsButton>
<MsButton v-permission="['PROJECT_TEST_PLAN:READ+UPDATE']" type="button" status="default">
<MsButton
v-permission="['PROJECT_TEST_PLAN:READ+UPDATE']"
type="button"
status="default"
:loading="followLoading"
@click="followHandler"
>
<MsIcon
:type="detail.followFlag ? 'icon-icon_collect_filled' : 'icon-icon_collection_outlined'"
:class="`mr-[8px] ${detail.followFlag ? 'text-[rgb(var(--warning-6))]' : ''}`"
@ -51,20 +67,21 @@
<span class="mr-[8px]">{{ t('testPlan.testPlanDetail.executed') }}</span>
<span v-if="detail.status === 'PREPARED'" class="text-[var(--color-text-1)]">-</span>
<span v-else>
<span class="font-medium text-[var(--color-text-1)]"> {{ detail.executedCount }} </span>/{{
detail.caseCount
}}
<span class="mr-1 font-medium text-[var(--color-text-1)]"> {{ hasExecutedCount }} </span>/<span
class="ml-1"
>{{ countDetail.caseTotal }}</span
>
</span>
</div>
<div class="text-[var(--color-text-4)]">
<span class="mr-[8px]">{{ t('caseManagement.caseReview.passRate') }}</span>
<span v-if="detail.status === 'PREPARED'" class="text-[var(--color-text-1)]">-</span>
<span v-if="detail.status === 'PREPARED'" class="text-[var(--color-text-1)]"></span>
<span v-else>
<span class="font-medium text-[var(--color-text-1)]"> {{ detail.passRate }}% </span>
<span class="font-medium text-[var(--color-text-1)]"> {{ countDetail.passRate }}% </span>
</span>
</div>
</div>
<passRateLine :review-detail="detail" height="8px" radius="var(--border-radius-mini)" />
<StatusProgress :status-detail="countDetail" height="8px" radius="var(--border-radius-mini)" />
</div>
</template>
<a-tabs v-model:active-key="activeTab" class="no-content">
@ -74,13 +91,25 @@
<!-- special-height的174: 上面卡片高度158 + mt的16 -->
<MsCard class="mt-[16px]" :special-height="174" simple has-breadcrumb no-content-padding>
<FeatureCase v-if="activeTab === 'featureCase'" />
<BugManagement v-if="activeTab === 'defectList'" :plan-id="detail.id" />
<!-- TODO 先不上 -->
<!-- <BugManagement v-if="activeTab === 'defectList'" :plan-id="detail.id" /> -->
</MsCard>
<AssociateDrawer v-model:visible="caseAssociateVisible" :associated-ids="hasSelectedIds" @success="success" />
<CreateAndEditPlanDrawer
v-model:visible="showPlanDrawer"
:plan-id="planId"
:is-copy="isCopy"
:module-tree="testPlanTree"
@load-plan-list="successHandler"
/>
<ActionModal v-model:visible="showStatusDeleteModal" :record="activeRecord" @success="okHandler" />
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import { useRoute } from 'vue-router';
import { useRoute, useRouter } from 'vue-router';
import { Message } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsCard from '@/components/pure/ms-card/index.vue';
@ -88,28 +117,69 @@
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
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 BugManagement from './bugManagement/index.vue';
import FeatureCase from './featureCase/index.vue';
import passRateLine from '@/views/case-management/caseReview/components/passRateLine.vue';
import CreateAndEditPlanDrawer from '@/views/test-plan/testPlan/createAndEditPlanDrawer.vue';
import { getTestPlanDetail } from '@/api/modules/test-plan/testPlan';
import { testPlanDefaultDetail } from '@/config/testPlan';
import {
archivedPlan,
associationCaseToPlan,
followPlanRequest,
getPlanPassRate,
getTestPlanDetail,
getTestPlanModule,
} from '@/api/modules/test-plan/testPlan';
import { initDetailCount, testPlanDefaultDetail } from '@/config/testPlan';
import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal';
import useAppStore from '@/store/modules/app';
import useUserStore from '@/store/modules/user';
import { characterLimit } from '@/utils';
import type { TestPlanDetail } from '@/models/testPlan/testPlan';
import { ModuleTreeNode } from '@/models/common';
import type {
AssociateCaseRequest,
PassRateCountDetail,
TestPlanDetail,
TestPlanItem,
} from '@/models/testPlan/testPlan';
const userStore = useUserStore();
const appStore = useAppStore();
const { openModal } = useModal();
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const loading = ref(false);
const planId = ref(route.query.id as string);
const detail = ref<TestPlanDetail>({
...testPlanDefaultDetail,
});
const countDetail = ref<PassRateCountDetail>({ ...initDetailCount });
const hasExecutedCount = computed(() => {
const { successCount, fakeErrorCount, errorCount, blockCount } = countDetail.value;
return successCount + fakeErrorCount + errorCount + blockCount;
});
//
async function getStatistics() {
try {
const result = await getPlanPassRate([planId.value]);
// eslint-disable-next-line prefer-destructuring
countDetail.value = result[0];
} catch (error) {
console.log(error);
}
}
async function initDetail() {
try {
loading.value = true;
detail.value = await getTestPlanDetail(planId.value);
getStatistics();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
@ -117,9 +187,6 @@
loading.value = false;
}
}
onMounted(() => {
initDetail();
});
const fullActions = [
{
@ -143,11 +210,53 @@
}
return fullActions.filter((e) => e.eventTag !== 'archive');
});
function archiveHandler() {
openModal({
type: 'warning',
title: t('common.archiveConfirmTitle', { name: characterLimit(detail.value.name) }),
content: t('testPlan.testPlanIndex.confirmArchivePlan'),
okText: t('common.archive'),
cancelText: t('common.cancel'),
okButtonProps: {
status: 'normal',
},
onBeforeOk: async () => {
try {
await archivedPlan(planId.value);
Message.success(t('common.batchArchiveSuccess'));
initDetail();
} catch (error) {
console.log(error);
}
},
hideCancel: false,
});
}
const showStatusDeleteModal = ref<boolean>(false);
const activeRecord = ref<TestPlanItem | TestPlanDetail | undefined>();
//
function deleteHandler() {
activeRecord.value = cloneDeep(detail.value);
showStatusDeleteModal.value = true;
}
//
function okHandler(isDelete: boolean) {
if (isDelete) {
router.back();
} else {
initDetail();
}
}
function handleMoreSelect(item: ActionsItem) {
switch (item.eventTag) {
case 'archive':
archiveHandler();
break;
case 'delete':
deleteHandler();
break;
default:
break;
@ -160,11 +269,81 @@
key: 'featureCase',
title: t('menu.caseManagement.featureCase'),
},
{
key: 'defectList',
title: t('caseManagement.featureCase.defectList'),
},
// TODO
// {
// key: 'defectList',
// title: t('caseManagement.featureCase.defectList'),
// },
]);
const hasSelectedIds = ref<string[]>([]);
const caseAssociateVisible = ref(false);
//
function linkCase() {
caseAssociateVisible.value = true;
}
const showPlanDrawer = ref(false);
// |
const isCopy = ref<boolean>(false);
function editorCopyHandler(copyFlog: boolean) {
isCopy.value = copyFlog;
showPlanDrawer.value = true;
}
const followLoading = ref<boolean>(false);
//
async function followHandler() {
try {
followLoading.value = true;
await followPlanRequest({
userId: userStore.id || '',
testPlanId: detail.value.id as string,
});
Message.success(
detail.value.followFlag
? t('caseManagement.caseReview.unFollowSuccess')
: t('caseManagement.caseReview.followSuccess')
);
detail.value.followFlag = !detail.value.followFlag;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
followLoading.value = false;
}
}
function successHandler() {
initDetail();
}
const testPlanTree = ref<ModuleTreeNode[]>([]);
async function initPlanTree() {
try {
testPlanTree.value = await getTestPlanModule({ projectId: appStore.currentProjectId });
} catch (error) {
console.log(error);
}
}
//
async function success(params: AssociateCaseRequest) {
try {
await associationCaseToPlan({
functionalSelectIds: params.selectIds,
testPlanId: planId.value,
});
Message.success(t('ms.case.associate.associateSuccess'));
caseAssociateVisible.value = false;
initDetail();
} catch (error) {
console.log(error);
}
}
onBeforeMount(() => {
initDetail();
initPlanTree();
});
</script>
<style lang="less" scoped>

View File

@ -89,9 +89,10 @@
:offspring-ids="offspringIds"
:active-folder-type="activeCaseType"
:modules-count="modulesCount"
:module-tree="folderTree"
:node-name="nodeName"
@init="initModulesCount"
@edit="handleEdit"
@edit-or-copy="handleEditOrCopy"
/>
</div>
</template>
@ -100,6 +101,7 @@
v-model:visible="showPlanDrawer"
:plan-id="planId"
:module-tree="folderTree"
:is-copy="isCopy"
@close="resetPlanId"
@load-plan-list="loadPlanList"
/>
@ -237,8 +239,10 @@
const planTableRef = ref<InstanceType<typeof PlanTable>>();
const planId = ref('');
function handleEdit(id: string) {
const isCopy = ref<boolean>(false);
function handleEditOrCopy(id: string, isCopyFlag: boolean) {
planId.value = id;
isCopy.value = isCopyFlag;
showPlanDrawer.value = true;
}
function resetPlanId() {

View File

@ -1,5 +1,7 @@
export default {
'testPlan.testPlanIndex.createTestPlan': 'create test plan',
'testPlan.testPlanIndex.updateTestPlan': 'update test plan',
'testPlan.testPlanIndex.copyTestPlan': 'copy test plan',
'testPlan.testPlanIndex.allTestPlan': 'All test Plans',
'testPlan.testPlanIndex.collapseAll': 'Collapse all submodules',
'testPlan.testPlanIndex.expandAll': 'Expand all submodules',
@ -56,7 +58,7 @@ export default {
'testPlan.testPlanIndex.defaultEnv': 'Default Environment',
'testPlan.testPlanIndex.newEnv': 'New Environment',
'testPlan.testPlanIndex.executionProgress': 'Execution progress',
'testPlan.testPlanIndex.tolerance': 'tolerance',
'testPlan.testPlanIndex.threshold': 'threshold',
'testPlan.testPlanIndex.TotalCases': 'Total use cases',
'testPlan.testPlanIndex.functionalUseCase': 'case',
'testPlan.testPlanIndex.apiCase': 'Api use case',
@ -89,6 +91,9 @@ export default {
'testPlan.featureCase.executor': 'Executor',
'testPlan.featureCase.changeExecutor': 'Change executor',
'testPlan.featureCase.sort': 'sort',
'testPlan.featureCase.executionHistory': 'Execution History',
'testPlan.featureCase.noBugDataTooltip': 'No related defects, please',
'testPlan.featureCase.noBugDataNewBug': 'New defect',
'testPlan.featureCase.disassociateTip': 'Are you sure to cancel the association {name}? ',
'testPlan.featureCase.disassociateTipContent':
'After cancellation, it will affect the statistics related to the test plan',

View File

@ -1,5 +1,7 @@
export default {
'testPlan.testPlanIndex.createTestPlan': '创建测试计划',
'testPlan.testPlanIndex.updateTestPlan': '更新测试计划',
'testPlan.testPlanIndex.copyTestPlan': '复制测试计划',
'testPlan.testPlanIndex.allTestPlan': '全部测试计划',
'testPlan.testPlanIndex.collapseAll': '收起全部子模块',
'testPlan.testPlanIndex.expandAll': '展开全部子模块',
@ -56,7 +58,7 @@ export default {
'testPlan.testPlanIndex.defaultEnv': '默认环境',
'testPlan.testPlanIndex.newEnv': '新环境',
'testPlan.testPlanIndex.executionProgress': '执行进度',
'testPlan.testPlanIndex.tolerance': '容错率',
'testPlan.testPlanIndex.threshold': '通过阈值',
'testPlan.testPlanIndex.TotalCases': '用例总数',
'testPlan.testPlanIndex.functionalUseCase': '功能用例',
'testPlan.testPlanIndex.apiCase': '接口用例',
@ -87,6 +89,9 @@ export default {
'testPlan.featureCase.executor': '执行人',
'testPlan.featureCase.changeExecutor': '修改执行人',
'testPlan.featureCase.sort': '排序',
'testPlan.featureCase.executionHistory': '执行历史',
'testPlan.featureCase.noBugDataTooltip': '暂无可关联缺陷,请 ',
'testPlan.featureCase.noBugDataNewBug': '新建缺陷',
'testPlan.featureCase.disassociateTip': '确认取消关联 { name } 吗?',
'testPlan.featureCase.disassociateTipContent': '取消后,影响测试计划相关统计',
'testPlan.featureCase.batchDisassociateTipContent': '取消后,再次关联,执行结果为:未执行',