feat(测试计划): 测试计划组联调部分接口&测试计划独立报告页面&测试计划关联用例页面部分和联调部分

This commit is contained in:
xinxin.wu 2024-06-05 21:24:13 +08:00 committed by Craftsman
parent 4e35887b1e
commit 65a2d5570d
38 changed files with 3340 additions and 335 deletions

View File

@ -16,6 +16,7 @@ import {
deletePlanUrl,
DeleteTestPlanModuleUrl,
DisassociateCaseUrl,
dragPlanOnGroupUrl,
ExecuteHistoryUrl,
followPlanUrl,
GenerateReportUrl,
@ -36,17 +37,22 @@ import {
planPassRateUrl,
RunFeatureCaseUrl,
SortFeatureCaseUrl,
TestPlanAndGroupCopyUrl,
TestPlanApiAssociatedPageUrl,
TestPlanAssociateBugUrl,
TestPlanCancelBugUrl,
TestPlanCaseAssociatedPageUrl,
TestPlanCaseDetailUrl,
TestPlanGroupOptionsUrl,
updateTestPlanModuleUrl,
UpdateTestPlanUrl,
} from '@/api/requrls/test-plan/testPlan';
import { ApiCaseDetail, ApiDefinitionDetail } from '@/models/apiTest/management';
import { ReviewUserItem } from '@/models/caseManagement/caseReview';
import type { CaseManagementTable, CreateOrUpdateModule, UpdateModule } from '@/models/caseManagement/featureCase';
import type { CommonList, MoveModules, TableQueryParams } from '@/models/common';
import { ModuleTreeNode } from '@/models/common';
import { DragSortParams, ModuleTreeNode } from '@/models/common';
import type {
AddTestPlanParams,
AssociateCaseRequestType,
@ -254,3 +260,24 @@ export function getPlanDetailApiScenarioList(data: PlanDetailFeatureCaseListQuer
export function getPlanDetailExecuteHistory(data: PlanDetailFeatureCaseListQueryParams) {
return MSR.post<CommonList<PlanDetailExecuteHistoryItem>>({ url: PlanDetailExecuteHistoryUrl, data });
}
// 功能用例-关联用例-接口用例-API
export function getTestPlanAssociationApiList(data: TableQueryParams) {
return MSR.post<CommonList<ApiDefinitionDetail>>({ url: TestPlanApiAssociatedPageUrl, data });
}
// 功能用例-关联用例-接口用例-CASE
export function getTestPlanAssociationCaseList(data: TableQueryParams) {
return MSR.post<CommonList<ApiCaseDetail>>({ url: TestPlanCaseAssociatedPageUrl, data });
}
// 测试计划-复制测试计划&测试计划组
export function testPlanAndGroupCopy(id: string) {
return MSR.get({ url: `${TestPlanAndGroupCopyUrl}/${id}` });
}
// 测试计划-测试计划组下拉列表
export function getPlanGroupOptions(projectId: string) {
return MSR.get({ url: `${TestPlanGroupOptionsUrl}/${projectId}` });
}
// 测试计划-测试计划组内拖拽
export function dragPlanOnGroup(data: DragSortParams) {
return MSR.post({ url: dragPlanOnGroupUrl, data });
}

View File

@ -80,3 +80,13 @@ export const BatchUpdateCaseExecutorUrl = '/test-plan/functional/case/batch/upda
export const ExecuteHistoryUrl = '/test-plan/functional/case/exec/history';
// 计划详情-执行历史 TODO 联调
export const PlanDetailExecuteHistoryUrl = '/api/scenario/execute/page';
// 功能用例-关联用例-接口用例-API
export const TestPlanApiAssociatedPageUrl = '/test-plan/association/api/page';
// 功能用例-关联用例-接口用例-CASE
export const TestPlanCaseAssociatedPageUrl = '/test-plan/association/api/case/page';
// 测试计划-复制
export const TestPlanAndGroupCopyUrl = '/test-plan/copy';
// 测试计划-计划组下拉
export const TestPlanGroupOptionsUrl = 'test-plan/group-list';
// 测试计划-拖拽测试计划
export const dragPlanOnGroupUrl = '/test-plan/sort';

View File

@ -0,0 +1,288 @@
<template>
<MsBaseTable
ref="tableRef"
class="mt-[16px]"
v-bind="propsRes"
:action-config="{
baseAction: [],
moreAction: [],
}"
v-on="propsEvent"
@filter-change="getModuleCount"
>
<template #num="{ record }">
<MsButton type="text">{{ record.num }}</MsButton>
</template>
<template #lastReportStatus="{ record }">
<ExecutionStatus
:module-type="ReportEnum.API_REPORT"
:status="record.lastReportStatus"
:class="[!record.lastReportId ? '' : 'cursor-pointer']"
/>
</template>
<template #[FilterSlotNameEnum.CASE_MANAGEMENT_CASE_LEVEL]="{ filterContent }">
<CaseLevel :case-level="filterContent.value" />
</template>
<template #caseLevel="{ record }">
<CaseLevel :case-level="record.priority" />
</template>
<template #[FilterSlotNameEnum.CASE_MANAGEMENT_EXECUTE_RESULT]="{ filterContent }">
<ExecuteResult :execute-result="filterContent.value" />
</template>
<template #lastExecResult="{ record }">
<ExecuteResult :execute-result="record.lastExecResult" />
</template>
<template #createName="{ record }">
<a-tooltip :content="`${record.createName}`" position="tl">
<div class="one-line-text">{{ characterLimit(record.createName) }}</div>
</a-tooltip>
</template>
<template #[FilterSlotNameEnum.API_TEST_CASE_API_LAST_EXECUTE_STATUS]="{ filterContent }">
<ExecutionStatus :module-type="ReportEnum.API_REPORT" :status="filterContent.value" />
</template>
</MsBaseTable>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { TableData } from '@arco-design/web-vue';
import MsButton from '@/components/pure/ms-button/index.vue';
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 CaseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
import ExecuteResult from '@/components/business/ms-case-associate/executeResult.vue';
import ExecutionStatus from '@/views/api-test/report/component/reportStatus.vue';
import { useI18n } from '@/hooks/useI18n';
import { characterLimit } from '@/utils';
import type { TableQueryParams } from '@/models/common';
import { CasePageApiTypeEnum } from '@/enums/associateCaseEnum';
import { CaseLinkEnum } from '@/enums/caseEnum';
import { ReportEnum, ReportStatus } from '@/enums/reportEnum';
import { FilterSlotNameEnum } from '@/enums/tableFilterEnum';
import { getPublicLinkCaseListMap } from './utils/page';
import { casePriorityOptions } from '@/views/api-test/components/config';
const { t } = useI18n();
const props = defineProps<{
associationType: string; // | |
activeModule: string;
offspringIds: string[];
currentProject: string;
associatedIds?: string[]; // ids
activeSourceType: keyof typeof CaseLinkEnum;
selectorAll?: boolean;
keyword: string;
getPageApiType: keyof typeof CasePageApiTypeEnum; // Api
extraTableParams?: TableQueryParams; //
}>();
const emit = defineEmits<{
(e: 'getModuleCount', params: TableQueryParams): void;
(e: 'refresh'): void;
(e: 'initModules'): void;
}>();
const lastReportStatusListOptions = computed(() => {
return Object.keys(ReportStatus).map((key) => {
return {
value: key,
...Object.keys(ReportStatus[key]),
};
});
});
const columns: MsTableColumn = [
{
title: 'ID',
dataIndex: 'num',
slotName: 'num',
sortIndex: 1,
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
fixed: 'left',
width: 100,
showTooltip: true,
columnSelectorDisabled: true,
},
{
title: 'case.caseName',
dataIndex: 'name',
showTooltip: true,
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
width: 180,
columnSelectorDisabled: true,
},
{
title: 'case.caseLevel',
dataIndex: 'priority',
slotName: 'caseLevel',
filterConfig: {
options: casePriorityOptions,
filterSlotName: FilterSlotNameEnum.CASE_MANAGEMENT_CASE_LEVEL,
},
width: 150,
showDrag: true,
},
{
title: 'case.lastReportStatus',
dataIndex: 'lastReportStatus',
slotName: 'lastReportStatus',
filterConfig: {
options: lastReportStatusListOptions.value,
filterSlotName: FilterSlotNameEnum.API_TEST_CASE_API_LAST_EXECUTE_STATUS,
},
showInTable: false,
width: 150,
showDrag: true,
},
{
title: 'caseManagement.featureCase.tableColumnCreateUser',
slotName: 'createName',
dataIndex: 'createName',
showTooltip: true,
width: 200,
showDrag: true,
},
{
title: 'caseManagement.featureCase.tableColumnCreateTime',
slotName: 'createTime',
dataIndex: 'createTime',
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
width: 200,
showDrag: true,
},
];
const getPageList = computed(() => {
return props.activeSourceType !== 'API'
? getPublicLinkCaseListMap[props.getPageApiType][props.activeSourceType]
: getPublicLinkCaseListMap[props.getPageApiType][props.activeSourceType].CASE;
});
function getCaseLevel(record: TableData) {
if (record.customFields && record.customFields.length) {
const caseItem = record.customFields.find((item: any) => item.fieldName === '用例等级' && item.internal);
return caseItem?.options.find((item: any) => item.value === caseItem?.defaultValue).text;
}
return undefined;
}
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector, setPagination, resetFilterParams } =
useTable(
getPageList.value,
{
columns,
showSetting: false,
selectable: true,
showSelectAll: true,
heightUsed: 310,
showSelectorAll: true,
},
(record) => {
return {
...record,
caseLevel: getCaseLevel(record),
tags: (record.tags || []).map((item: string, i: number) => {
return {
id: `${record.id}-${i}`,
name: item,
};
}),
};
}
);
async function getTableParams() {
return {
keyword: props.keyword,
projectId: props.currentProject,
protocol: 'HTTP',
moduleIds: props.activeModule === 'all' || !props.activeModule ? [] : [props.activeModule, ...props.offspringIds],
excludeIds: [...(props.associatedIds || [])], // id
condition: {
keyword: props.keyword,
},
...props.extraTableParams,
};
}
async function getModuleCount() {
const tableParams = await getTableParams();
emit('getModuleCount', {
...tableParams,
current: propsRes.value.msPagination?.current,
pageSize: propsRes.value.msPagination?.pageSize,
});
}
async function loadCaseList() {
const tableParams = await getTableParams();
setLoadListParams(tableParams);
loadList();
emit('getModuleCount', {
...tableParams,
current: propsRes.value.msPagination?.current,
pageSize: propsRes.value.msPagination?.pageSize,
});
}
const tableRef = ref<InstanceType<typeof MsBaseTable>>();
watch(
() => props.activeSourceType,
(val) => {
if (val) {
tableRef.value?.initColumn(columns);
resetSelector();
resetFilterParams();
setPagination({
current: 1,
});
}
}
);
watch(
() => props.currentProject,
(val) => {
if (val) {
loadCaseList();
}
},
{
immediate: true,
}
);
function getApiCaseSaveParams() {
const { excludeKeys, selectedKeys, selectorStatus } = propsRes.value;
const tableParams = getTableParams();
return {
...tableParams,
excludeIds: [...excludeKeys].concat(...(props.associatedIds || [])),
selectIds: selectorStatus === 'all' ? [] : [...selectedKeys],
selectAll: selectorStatus === 'all',
};
}
defineExpose({
getApiCaseSaveParams,
loadCaseList,
});
</script>
<style scoped></style>

View File

@ -0,0 +1,259 @@
<template>
<MsBaseTable
v-if="props.showType === 'API'"
ref="apiTableRef"
class="mt-[16px]"
v-bind="propsRes"
:action-config="{
baseAction: [],
moreAction: [],
}"
v-on="propsEvent"
@filter-change="getModuleCount"
>
<template #num="{ record }">
<MsButton type="text">{{ record.num }}</MsButton>
</template>
<template #[FilterSlotNameEnum.API_TEST_API_REQUEST_METHODS]="{ filterContent }">
<apiMethodName :method="filterContent.value" />
</template>
<template #method="{ record }">
<apiMethodName :method="record.method" is-tag />
</template>
<template #caseTotal="{ record }">
{{ record.caseTotal }}
</template>
<template #createUserName="{ record }">
<a-tooltip :content="`${record.createUserName}`" position="tl">
<div class="one-line-text">{{ record.createUserName }}</div>
</a-tooltip>
</template>
</MsBaseTable>
</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 { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import type { TableQueryParams } from '@/models/common';
import { RequestMethods } from '@/enums/apiEnum';
import { CasePageApiTypeEnum } from '@/enums/associateCaseEnum';
import { CaseLinkEnum } from '@/enums/caseEnum';
import { FilterRemoteMethodsEnum, FilterSlotNameEnum } from '@/enums/tableFilterEnum';
import { getPublicLinkCaseListMap } from './utils/page';
const { t } = useI18n();
const appStore = useAppStore();
const props = defineProps<{
associationType: string; // | |
activeModule: string;
offspringIds: string[];
currentProject: string;
associatedIds?: string[]; // ids
activeSourceType: keyof typeof CaseLinkEnum;
selectorAll?: boolean;
keyword: string;
showType: string;
getPageApiType: keyof typeof CasePageApiTypeEnum; // Api
extraTableParams?: TableQueryParams; //
}>();
const emit = defineEmits<{
(e: 'getModuleCount', params: TableQueryParams): void;
(e: 'refresh'): void;
(e: 'initModules'): void;
}>();
const requestMethodsOptions = computed(() => {
return Object.values(RequestMethods).map((e) => {
return {
value: e,
key: e,
};
});
});
const columns: MsTableColumn = [
{
title: 'ID',
dataIndex: 'num',
slotName: 'num',
sortIndex: 1,
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
fixed: 'left',
width: 100,
columnSelectorDisabled: true,
},
{
title: 'apiTestManagement.apiName',
dataIndex: 'name',
showTooltip: true,
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
width: 200,
columnSelectorDisabled: true,
},
{
title: 'apiTestManagement.apiType',
dataIndex: 'method',
slotName: 'method',
width: 140,
showDrag: true,
filterConfig: {
options: requestMethodsOptions.value,
filterSlotName: FilterSlotNameEnum.API_TEST_API_REQUEST_METHODS,
},
},
{
title: 'apiTestManagement.path',
dataIndex: 'path',
showTooltip: true,
width: 200,
showDrag: true,
},
{
title: 'common.tag',
dataIndex: 'tags',
isTag: true,
isStringTag: true,
width: 400,
showDrag: true,
},
{
title: 'apiTestManagement.caseTotal',
dataIndex: 'caseTotal',
showTooltip: true,
width: 100,
showDrag: true,
slotName: 'caseTotal',
},
{
title: 'common.creator',
slotName: 'createUserName',
dataIndex: 'createUser',
filterConfig: {
mode: 'remote',
loadOptionParams: {
projectId: appStore.currentProjectId,
},
remoteMethod: FilterRemoteMethodsEnum.PROJECT_PERMISSION_MEMBER,
placeholderText: t('caseManagement.featureCase.PleaseSelect'),
},
showInTable: true,
width: 200,
showDrag: true,
},
];
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector, setPagination, resetFilterParams } =
useTable(getPublicLinkCaseListMap[props.getPageApiType][props.activeSourceType].API, {
columns,
showSetting: false,
selectable: true,
showSelectAll: true,
heightUsed: 310,
showSelectorAll: true,
});
async function getTableParams() {
return {
keyword: props.keyword,
projectId: props.currentProject,
protocol: 'HTTP',
moduleIds: props.activeModule === 'all' || !props.activeModule ? [] : [props.activeModule, ...props.offspringIds],
excludeIds: [...(props.associatedIds || [])], // id
condition: {
keyword: props.keyword,
},
...props.extraTableParams,
};
}
async function getModuleCount() {
const tableParams = await getTableParams();
emit('getModuleCount', {
...tableParams,
current: propsRes.value.msPagination?.current,
pageSize: propsRes.value.msPagination?.pageSize,
});
}
async function loadApiList() {
const tableParams = await getTableParams();
setLoadListParams(tableParams);
loadList();
emit('getModuleCount', {
...tableParams,
current: propsRes.value.msPagination?.current,
pageSize: propsRes.value.msPagination?.pageSize,
});
}
watch(
() => props.activeSourceType,
(val) => {
if (val) {
resetSelector();
resetFilterParams();
setPagination({
current: 1,
});
}
}
);
watch(
() => props.currentProject,
(val) => {
if (val) {
loadApiList();
}
},
{
immediate: true,
}
);
watch(
() => props.showType,
(val) => {
if (val === 'API') {
resetSelector();
resetFilterParams();
loadApiList();
}
}
);
function getApiSaveParams() {
const { excludeKeys, selectedKeys, selectorStatus } = propsRes.value;
const tableParams = getTableParams();
return {
...tableParams,
excludeIds: [...excludeKeys].concat(...(props.associatedIds || [])),
selectIds: selectorStatus === 'all' ? [] : [...selectedKeys],
selectAll: selectorStatus === 'all',
};
}
defineExpose({
getApiSaveParams,
loadApiList,
});
</script>
<style scoped></style>

View File

@ -0,0 +1,300 @@
<template>
<MsBaseTable
ref="tableRef"
class="mt-[16px]"
v-bind="propsRes"
:action-config="{
baseAction: [],
moreAction: [],
}"
v-on="propsEvent"
@filter-change="getModuleCount"
>
<template #num="{ record }">
<MsButton type="text">{{ record.num }}</MsButton>
</template>
<template #reviewStatus="{ record }">
<MsIcon
:type="statusIconMap[record.reviewStatus]?.icon || ''"
class="mr-1"
:class="[statusIconMap[record.reviewStatus].color]"
></MsIcon>
<span>{{ statusIconMap[record.reviewStatus]?.statusText || '' }} </span>
</template>
<template #lastExecuteResult="{ record }">
<ExecuteResult v-if="record.lastExecuteResult" :execute-result="record.lastExecuteResult" />
<span v-else>-</span>
</template>
<template #[FilterSlotNameEnum.CASE_MANAGEMENT_CASE_LEVEL]="{ filterContent }">
<CaseLevel :case-level="filterContent.value" />
</template>
<template #caseLevel="{ record }">
<CaseLevel :case-level="record.caseLevel" />
</template>
<template #[FilterSlotNameEnum.CASE_MANAGEMENT_EXECUTE_RESULT]="{ filterContent }">
<ExecuteResult :execute-result="filterContent.value" />
</template>
<template #lastExecResult="{ record }">
<ExecuteResult :execute-result="record.lastExecResult" />
</template>
</MsBaseTable>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { TableData } from '@arco-design/web-vue';
import MsButton from '@/components/pure/ms-button/index.vue';
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 CaseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
import ExecuteResult from '@/components/business/ms-case-associate/executeResult.vue';
import { useI18n } from '@/hooks/useI18n';
import type { TableQueryParams } from '@/models/common';
import { CasePageApiTypeEnum } from '@/enums/associateCaseEnum';
import { CaseLinkEnum } from '@/enums/caseEnum';
import { FilterSlotNameEnum } from '@/enums/tableFilterEnum';
import { getPublicLinkCaseListMap } from './utils/page';
import { casePriorityOptions } from '@/views/api-test/components/config';
import { executionResultMap, statusIconMap } from '@/views/case-management/caseManagementFeature/components/utils';
const { t } = useI18n();
const props = defineProps<{
associationType: string; // | |
activeModule: string;
offspringIds: string[];
currentProject: string;
associatedIds?: string[]; // ids
activeSourceType: keyof typeof CaseLinkEnum;
keyword: string;
getPageApiType: keyof typeof CasePageApiTypeEnum; // Api
extraTableParams?: TableQueryParams; //
}>();
const emit = defineEmits<{
(e: 'getModuleCount', params: TableQueryParams): void;
(e: 'refresh'): void;
(e: 'initModules'): void;
}>();
const reviewResultOptions = computed(() => {
return Object.keys(statusIconMap).map((key) => {
return {
value: key,
label: statusIconMap[key].statusText,
};
});
});
const executeResultOptions = computed(() => {
return Object.keys(executionResultMap).map((key) => {
return {
value: key,
label: executionResultMap[key].statusText,
};
});
});
const columns: MsTableColumn = [
{
title: 'ID',
dataIndex: 'num',
slotName: 'num',
sortIndex: 1,
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
fixed: 'left',
width: 100,
showTooltip: true,
columnSelectorDisabled: true,
},
{
title: 'case.caseName',
dataIndex: 'name',
showTooltip: true,
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
width: 180,
columnSelectorDisabled: true,
},
{
title: 'case.caseLevel',
dataIndex: 'caseLevel',
slotName: 'caseLevel',
filterConfig: {
options: casePriorityOptions,
filterSlotName: FilterSlotNameEnum.CASE_MANAGEMENT_CASE_LEVEL,
},
width: 150,
showDrag: true,
},
{
title: 'caseManagement.featureCase.tableColumnReviewResult',
dataIndex: 'reviewStatus',
slotName: 'reviewStatus',
filterConfig: {
options: reviewResultOptions.value,
filterSlotName: FilterSlotNameEnum.CASE_MANAGEMENT_REVIEW_RESULT,
},
showInTable: true,
width: 150,
showDrag: true,
},
{
title: 'caseManagement.featureCase.tableColumnExecutionResult',
dataIndex: 'lastExecuteResult',
slotName: 'lastExecuteResult',
filterConfig: {
options: executeResultOptions.value,
filterSlotName: FilterSlotNameEnum.CASE_MANAGEMENT_EXECUTE_RESULT,
},
showInTable: true,
width: 150,
showDrag: true,
},
{
title: 'caseManagement.featureCase.tableColumnCreateUser',
slotName: 'createUserName',
dataIndex: 'createUserName',
showTooltip: true,
width: 200,
showDrag: true,
},
{
title: 'caseManagement.featureCase.tableColumnCreateTime',
slotName: 'createTime',
dataIndex: 'createTime',
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
width: 200,
showDrag: true,
},
];
const getPageList = computed(() => {
return getPublicLinkCaseListMap[props.getPageApiType][props.activeSourceType];
});
function getCaseLevel(record: TableData) {
if (record.customFields && record.customFields.length) {
const caseItem = record.customFields.find((item: any) => item.fieldName === '用例等级' && item.internal);
return caseItem?.options.find((item: any) => item.value === caseItem?.defaultValue).text;
}
return undefined;
}
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector, setPagination, resetFilterParams } =
useTable(
getPageList.value,
{
columns,
showSetting: false,
selectable: true,
showSelectAll: true,
heightUsed: 310,
showSelectorAll: true,
},
(record) => {
return {
...record,
caseLevel: getCaseLevel(record),
tags: (record.tags || []).map((item: string, i: number) => {
return {
id: `${record.id}-${i}`,
name: item,
};
}),
};
}
);
async function getTableParams() {
return {
keyword: props.keyword,
projectId: props.currentProject,
moduleIds: props.activeModule === 'all' || !props.activeModule ? [] : [props.activeModule, ...props.offspringIds],
excludeIds: [...(props.associatedIds || [])], // id
condition: {
keyword: props.keyword,
filter: propsRes.value.filter,
},
...props.extraTableParams,
};
}
async function getModuleCount() {
const tableParams = await getTableParams();
emit('getModuleCount', {
...tableParams,
current: propsRes.value.msPagination?.current,
pageSize: propsRes.value.msPagination?.pageSize,
});
}
async function loadCaseList() {
const tableParams = await getTableParams();
setLoadListParams(tableParams);
loadList();
emit('getModuleCount', {
...tableParams,
current: propsRes.value.msPagination?.current,
pageSize: propsRes.value.msPagination?.pageSize,
});
}
const tableRef = ref<InstanceType<typeof MsBaseTable>>();
function getFunctionalSaveParams() {
const { excludeKeys, selectedKeys, selectorStatus } = propsRes.value;
const tableParams = getTableParams();
return {
...tableParams,
excludeIds: [...excludeKeys].concat(...(props.associatedIds || [])),
selectIds: selectorStatus === 'all' ? [] : [...selectedKeys],
selectAll: selectorStatus === 'all',
};
}
watch(
() => props.activeSourceType,
(val) => {
if (val) {
tableRef.value?.initColumn(columns);
resetSelector();
resetFilterParams();
setPagination({
current: 1,
});
}
}
);
watch(
() => props.currentProject,
(val) => {
if (val) {
loadCaseList();
}
},
{
immediate: true,
}
);
defineExpose({
getFunctionalSaveParams,
loadCaseList,
});
</script>
<style scoped></style>

View File

@ -0,0 +1,233 @@
<template>
<MsFolderAll
:active-folder="activeFolder"
:folder-name="t('caseManagement.caseReview.allCases')"
:all-count="allCount"
@set-active-folder="setActiveFolder"
>
</MsFolderAll>
<a-divider class="my-[8px]" />
<div class="mb-[8px] flex items-center gap-[8px]">
<a-input
v-model:model-value="moduleKeyword"
:placeholder="t('caseManagement.caseReview.folderSearchPlaceholder')"
allow-clear
:max-length="255"
/>
<a-tooltip :content="isExpandAll ? t('apiScenario.collapseAll') : t('apiScenario.expandAllStep')">
<a-button
type="outline"
class="expand-btn arco-btn-outline--secondary"
@click="() => (isExpandAll = !isExpandAll)"
>
<MsIcon v-if="isExpandAll" type="icon-icon_comment_collapse_text_input" />
<MsIcon v-else type="icon-icon_comment_expand_text_input" />
</a-button>
</a-tooltip>
</div>
<a-spin class="w-full" :loading="moduleLoading">
<MsTree
v-model:selected-keys="selectedKeys"
:data="caseTree"
:keyword="moduleKeyword"
:empty-text="t('common.noData')"
:virtual-list-props="virtualListProps"
:field-names="{
title: 'name',
key: 'id',
children: 'children',
count: 'count',
}"
:expand-all="isExpandAll"
block-node
title-tooltip-position="top"
@select="folderNodeSelect"
>
<template #title="nodeData">
<div class="inline-flex w-full gap-[8px]">
<div class="one-line-text w-full text-[var(--color-text-1)]">{{ nodeData.name }}</div>
<div class="ms-tree-node-count ml-[4px] text-[var(--color-text-brand)]">{{ nodeData.count || 0 }}</div>
</div>
</template>
</MsTree>
</a-spin>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useVModel } from '@vueuse/core';
import MsFolderAll from '@/components/business/ms-folder-all/index.vue';
import MsTree from '@/components/business/ms-tree/index.vue';
import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
import { useI18n } from '@/hooks/useI18n';
import { mapTree } from '@/utils';
import { ModuleTreeNode } from '@/models/common';
import { CaseModulesApiTypeEnum } from '@/enums/associateCaseEnum';
import { CaseLinkEnum } from '@/enums/caseEnum';
import { getModuleTreeFunc } from './utils/moduleTree';
const { t } = useI18n();
const props = defineProps<{
modulesCount?: Record<string, number>; //
selectedKeys: string[]; // key
currentProject: string;
getModulesApiType: CaseModulesApiTypeEnum[keyof CaseModulesApiTypeEnum];
activeTab: keyof typeof CaseLinkEnum;
extraModulesParams?: Record<string, any>; //
}>();
const emit = defineEmits<{
(e: 'folderNodeSelect', ids: string[], _offspringIds: string[], nodeName?: string): void;
(e: 'init', params: ModuleTreeNode[]): void;
}>();
const selectedKeys = useVModel(props, 'selectedKeys', emit);
const moduleKeyword = ref('');
const activeFolder = ref<string>('all');
const allCount = ref(0);
const isExpandAll = ref(false);
const caseTree = ref<ModuleTreeNode[]>([]);
const moduleLoading = ref(false);
const virtualListProps = computed(() => {
return {
height: 'calc(100vh - 408px)',
threshold: 200,
fixedSize: true,
buffer: 15, // 10 padding
};
});
function setActiveFolder(id: string) {
activeFolder.value = id;
emit('folderNodeSelect', [id], [], t('caseManagement.featureCase.allCase'));
}
/**
* 处理文件夹树节点选中事件
*/
function folderNodeSelect(_selectedKeys: (string | number)[], node: MsTreeNodeData) {
const offspringIds: string[] = [];
mapTree(node.children || [], (e) => {
offspringIds.push(e.id);
return e;
});
activeFolder.value = node.id;
emit('folderNodeSelect', _selectedKeys as string[], offspringIds, node.name);
}
/**
* 初始化模块树
*/
async function initModules() {
try {
moduleLoading.value = true;
const res = await getModuleTreeFunc(props.getModulesApiType, props.activeTab, {
projectId: props.currentProject,
...props.extraModulesParams,
});
caseTree.value = mapTree<ModuleTreeNode>(res, (node) => {
return {
...node,
count: props.modulesCount?.[node.id] || 0,
};
});
emit('init', caseTree.value);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
moduleLoading.value = false;
}
}
/**
* 初始化模块文件数量
*/
watch(
() => props.modulesCount,
(obj) => {
caseTree.value = mapTree<ModuleTreeNode>(caseTree.value, (node) => {
return {
...node,
count: obj?.[node.id] || 0,
};
});
allCount.value = obj?.all || 0;
}
);
watchEffect(() => {
if (props.currentProject) {
initModules();
}
});
watch(
() => props.activeTab,
(val) => {
if (val) {
initModules();
}
}
);
</script>
<style scoped lang="less">
.folder {
@apply flex cursor-pointer items-center justify-between;
padding: 8px 4px;
border-radius: var(--border-radius-small);
&:hover {
background-color: rgb(var(--primary-1));
}
.folder-text {
@apply flex cursor-pointer items-center;
.folder-icon {
margin-right: 4px;
color: var(--color-text-4);
}
.folder-name {
color: var(--color-text-1);
}
.folder-count {
margin-left: 4px;
color: var(--color-text-4);
}
}
.folder-text--active {
.folder-icon,
.folder-name,
.folder-count {
color: rgb(var(--primary-5));
}
}
}
.footer {
@apply flex items-center justify-between;
margin: auto -16px -16px;
padding: 12px 16px;
box-shadow: 0 -1px 4px 0 rgb(31 35 41 / 10%);
}
.expand-btn {
padding: 8px;
.arco-icon {
color: var(--color-text-4);
}
&:hover {
border-color: rgb(var(--primary-5)) !important;
background-color: rgb(var(--primary-1)) !important;
.arco-icon {
color: rgb(var(--primary-5));
}
}
}
</style>

View File

@ -0,0 +1,497 @@
<template>
<MsDrawer
v-model:visible="innerVisible"
:title="t('ms.case.associate.title')"
:width="1200"
:footer="false"
no-content-padding
unmount-on-close
>
<template #headerLeft>
<div class="float-left">
<a-select
v-model="innerProject"
class="ml-2 w-[240px]"
:default-value="innerProject"
allow-search
:placeholder="t('common.pleaseSelect')"
>
<template #arrow-icon>
<icon-caret-down />
</template>
<a-tooltip v-for="item of projectList" :key="item.id" :mouse-enter-delay="500" :content="item.name">
<a-option :value="item.id" :class="item.id === innerProject ? 'arco-select-option-selected' : ''">
{{ item.name }}
</a-option>
</a-tooltip>
</a-select>
</div>
</template>
<MsTab
v-model:active-key="activeTab"
:show-badge="false"
:content-tab-list="contentTabList"
class="no-content relative border-b"
/>
<div class="flex h-[calc(100vh-104px)]">
<div class="w-[292px] border-r border-[var(--color-text-n8)] p-[16px]">
<CaseTree
ref="caseTreeRef"
:modules-count="modulesCount"
:selected-keys="selectedKeys"
:get-modules-api-type="props.getModulesApiType"
:current-project="innerProject"
:active-tab="activeTab"
:extra-modules-params="props.extraModulesParams"
@folder-node-select="handleFolderNodeSelect"
@init="initModuleTree"
/>
</div>
<div class="flex w-[calc(100%-293px)] flex-col p-[16px]">
<MsAdvanceFilter
v-model:keyword="keyword"
:filter-config-list="[]"
:custom-fields-config-list="[]"
:row-count="0"
:search-placeholder="t('ms.case.associate.searchPlaceholder')"
@keyword-search="loadCaseList"
@adv-search="loadCaseList"
@refresh="loadCaseList"
>
<template #left>
<div class="flex w-full items-center justify-between">
<a-radio-group v-if="activeTab === 'API'" v-model="showType" type="button" class="file-show-type mr-2">
<a-radio value="API" class="show-type-icon p-[2px]">API</a-radio>
<a-radio value="CASE" class="show-type-icon p-[2px]">CASE</a-radio>
</a-radio-group>
<a-popover v-else title="" position="bottom">
<div class="flex">
<div class="one-line-text mr-1 max-h-[32px] max-w-[300px] text-[var(--color-text-1)]">
{{ activeFolderName }}
</div>
<span class="text-[var(--color-text-4)]"> ({{ modulesCount[activeFolder] || 0 }})</span>
</div>
<template #content>
<div class="max-w-[400px] text-[14px] font-medium text-[var(--color-text-1)]">
{{ activeFolderName }}
<span class="text-[var(--color-text-4)]">({{ modulesCount[activeFolder] || 0 }})</span>
</div>
</template>
</a-popover>
<a-checkbox v-if="activeTab === 'FUNCTIONAL'" v-model="isAddAssociatedCase">
<div class="flex items-center">
{{ t('ms.case.associate.addAssociatedCase') }}
<a-tooltip position="top" :content="t('ms.case.associate.automaticallyAddApiCase')">
<icon-question-circle
class="ml-[4px] mr-[12px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
</a-tooltip>
</div>
</a-checkbox>
</div>
</template>
</MsAdvanceFilter>
<!-- 功能用例 -->
<CaseTable
v-if="activeTab === CaseLinkEnum.FUNCTIONAL"
ref="functionalTableRef"
:association-type="associateType"
:get-page-api-type="getPageApiType"
:active-module="activeFolder"
:offspring-ids="offspringIds"
:current-project="innerProject"
:associated-ids="props.associatedIds"
:active-source-type="activeTab"
:extra-table-params="props.extraTableParams"
:keyword="keyword"
@get-module-count="initModulesCount"
/>
<!-- 接口用例 API -->
<ApiTable
v-if="activeTab === CaseLinkEnum.API && showType === 'API'"
ref="apiTableRef"
:get-page-api-type="getPageApiType"
:extra-table-params="props.extraTableParams"
:association-type="associateType"
:active-module="activeFolder"
:offspring-ids="offspringIds"
:current-project="innerProject"
:associated-ids="props.associatedIds"
:active-source-type="activeTab"
:keyword="keyword"
:show-type="showType"
@get-module-count="initModulesCount"
/>
<!-- 接口用例 CASE -->
<ApiCaseTable
v-if="activeTab === CaseLinkEnum.API && showType === 'CASE'"
ref="caseTableRef"
:get-page-api-type="getPageApiType"
:extra-table-params="props.extraTableParams"
:association-type="associateType"
:active-module="activeFolder"
:offspring-ids="offspringIds"
:current-project="innerProject"
:associated-ids="props.associatedIds"
:active-source-type="activeTab"
:keyword="keyword"
:show-type="showType"
@get-module-count="initModulesCount"
/>
<!-- 接口场景用例 -->
<ScenarioCaseTable
v-if="activeTab === CaseLinkEnum.SCENARIO"
ref="scenarioTableRef"
:association-type="associateType"
:modules-count="modulesCount"
:active-module="activeFolder"
:offspring-ids="offspringIds"
:current-project="innerProject"
:associated-ids="props.associatedIds"
:active-source-type="activeTab"
:keyword="keyword"
@get-module-count="initModulesCount"
/>
<div class="footer">
<div class="flex flex-1 items-center">
<slot name="footerLeft">
<a-form ref="formRef" :model="form" layout="vertical" class="mb-0 max-w-[260px]">
<a-form-item
field="name"
hide-label
class="test-set-form-item"
:rules="[{ required: true, message: t('project.commonScript.publicScriptNameNotEmpty') }]"
>
<a-input-group class="w-full">
<div class="test-set h-[32px] w-[80px]">{{ t('ms.case.associate.testSet') }}</div>
<a-select
v-model="form.testMap"
class="max-w-[260px]"
:default-value="innerProject"
allow-search
:placeholder="t('common.pleaseSelect')"
>
<template #arrow-icon>
<icon-caret-down />
</template>
<a-tooltip
v-for="item of testList"
:key="item.value"
:mouse-enter-delay="500"
:content="item.name"
>
<a-option
:value="item.value"
:class="item.value === form.testMap ? 'arco-select-option-selected' : ''"
>
{{ item.label }}
</a-option>
</a-tooltip>
</a-select>
</a-input-group>
</a-form-item>
</a-form>
</slot>
</div>
<div class="flex items-center">
<slot name="footerRight">
<a-button type="secondary" :disabled="props.confirmLoading" class="mr-[12px]" @click="cancel">
{{ t('common.cancel') }}
</a-button>
<a-button :loading="props.confirmLoading" type="primary" @click="handleConfirm">
{{ t('ms.case.associate.associate') }}
</a-button>
</slot>
</div>
</div>
</div>
</div>
</MsDrawer>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useVModel } from '@vueuse/core';
import { FormInstance, Message, SelectOptionData, ValidatedError } from '@arco-design/web-vue';
import { MsAdvanceFilter } from '@/components/pure/ms-advance-filter';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsTab from '@/components/pure/ms-tab/index.vue';
import ApiCaseTable from './apiCaseTable.vue';
import ApiTable from './apiTable.vue';
import CaseTable from './caseTable.vue';
import CaseTree from './caseTree.vue';
import ScenarioCaseTable from './scenarioCaseTable.vue';
import { getAssociatedProjectOptions, getCustomFieldsTable } from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import type { ModuleTreeNode, TableQueryParams } from '@/models/common';
import type { ProjectListItem } from '@/models/setting/project';
import { CaseModulesApiTypeEnum, CasePageApiTypeEnum } from '@/enums/associateCaseEnum';
import { CaseLinkEnum } from '@/enums/caseEnum';
import { initGetModuleCountFunc } from './utils/moduleCount';
const appStore = useAppStore();
const { t } = useI18n();
const props = defineProps<{
visible: boolean;
projectId: string; // id
caseId?: string; // id
getModulesApiType: CaseModulesApiTypeEnum[keyof CaseModulesApiTypeEnum]; // Api
extraModulesParams?: Record<string, any>; //
getPageApiType: keyof typeof CasePageApiTypeEnum; // Api
extraTableParams?: TableQueryParams; //
getModuleCountApiType: CasePageApiTypeEnum[keyof CasePageApiTypeEnum]; // countApi
extraModuleCountParams?: TableQueryParams; //
okButtonDisabled?: boolean; //
confirmLoading?: boolean;
associatedIds?: string[]; // id
hideProjectSelect?: boolean; //
}>();
const emit = defineEmits<{
(e: 'update:visible', val: boolean): void;
(e: 'update:projectId', val: string): void;
(e: 'update:currentSelectCase', val: string | number | Record<string, any> | undefined): void;
(e: 'init', val: TableQueryParams): void; //
(e: 'close'): void;
(e: 'save', params: any): void; // table
}>();
const projectList = ref<ProjectListItem[]>([]);
const keyword = ref<string>('');
const innerProject = useVModel(props, 'projectId', emit);
const showType = ref('API');
const innerVisible = useVModel(props, 'visible', emit);
const associateType = ref<string>('project');
const modulesCount = ref<Record<string, any>>({});
const activeTab = ref<keyof typeof CaseLinkEnum>(CaseLinkEnum.FUNCTIONAL);
const form = ref({
type: t('ms.case.associate.testSet'),
testMap: '',
});
const testList = ref<SelectOptionData>([]);
const contentTabList = [
{
value: CaseLinkEnum.FUNCTIONAL,
label: t('ms.case.associate.functionalCase'),
},
{
value: CaseLinkEnum.API,
label: t('ms.case.associate.apiCase'),
},
{
value: CaseLinkEnum.SCENARIO,
label: t('ms.case.associate.apiScenarioCase'),
},
];
const activeFolder = ref('all');
const activeFolderName = ref(t('ms.case.associate.allCase'));
const selectedKeys = computed({
get: () => [activeFolder.value],
set: (val) => val,
});
/**
* 处理模块树节点选中事件
*/
const offspringIds = ref<string[]>([]);
function handleFolderNodeSelect(ids: string[], _offspringIds: string[], name?: string) {
[activeFolder.value] = ids;
offspringIds.value = [..._offspringIds];
activeFolderName.value = name ?? '';
}
const moduleTree = ref<ModuleTreeNode[]>([]);
function initModuleTree(tree: ModuleTreeNode[]) {
moduleTree.value = unref(tree);
}
const isAddAssociatedCase = ref<boolean>(false);
const formRef = ref<FormInstance | null>(null);
const functionalTableRef = ref<InstanceType<typeof CaseTable>>();
const apiTableRef = ref<InstanceType<typeof ApiTable>>();
const caseTableRef = ref<InstanceType<typeof ApiCaseTable>>();
const scenarioTableRef = ref<InstanceType<typeof ScenarioCaseTable>>();
function makeParams() {
switch (activeTab.value) {
case CaseLinkEnum.FUNCTIONAL:
return functionalTableRef.value?.getFunctionalSaveParams();
case CaseLinkEnum.API:
return showType.value === 'API'
? apiTableRef.value?.getApiSaveParams()
: caseTableRef.value?.getApiCaseSaveParams();
case CaseLinkEnum.SCENARIO:
return scenarioTableRef.value?.getScenarioSaveParams();
default:
break;
}
}
//
function handleConfirm() {
const params = makeParams();
if (!params?.selectIds.length) {
return;
}
formRef.value?.validate(async (errors: undefined | Record<string, ValidatedError>) => {
if (!errors) {
// emit('save', params);
}
});
// TODO:
emit('save', params);
}
function cancel() {
innerVisible.value = false;
keyword.value = '';
activeFolder.value = 'all';
activeFolderName.value = t('ms.case.associate.allCase');
formRef.value?.resetFields();
emit('close');
}
async function initProjectList(setDefault: boolean) {
try {
projectList.value = await getAssociatedProjectOptions(appStore.currentOrgId, activeTab.value);
if (setDefault) {
innerProject.value = projectList.value[0].id;
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
async function initModulesCount(params: TableQueryParams) {
try {
modulesCount.value = await initGetModuleCountFunc(props.getModuleCountApiType, activeTab.value, {
...params,
...props.extraModuleCountParams,
});
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
watch(
() => activeTab.value,
(val) => {
if (val) {
showType.value = 'API';
activeFolder.value = 'all';
initProjectList(true);
}
}
);
watch(
() => props.visible,
(val) => {
if (val) {
initProjectList(false);
innerProject.value = appStore.currentProjectId;
}
activeTab.value = CaseLinkEnum.FUNCTIONAL;
}
);
watch(
() => innerProject.value,
(val) => {
if (val) {
activeFolder.value = 'all';
}
}
);
function loadCaseList() {
switch (activeTab.value) {
case CaseLinkEnum.FUNCTIONAL:
return functionalTableRef.value?.loadCaseList();
case CaseLinkEnum.API:
return showType.value === 'API' ? apiTableRef.value?.loadApiList() : caseTableRef.value?.loadCaseList();
case CaseLinkEnum.SCENARIO:
return scenarioTableRef.value?.loadScenarioList();
default:
break;
}
}
</script>
<style scoped lang="less">
.folder {
@apply flex cursor-pointer items-center justify-between;
padding: 8px 4px;
border-radius: var(--border-radius-small);
&:hover {
background-color: rgb(var(--primary-1));
}
.folder-text {
@apply flex cursor-pointer items-center;
.folder-icon {
margin-right: 4px;
color: var(--color-text-4);
}
.folder-name {
color: var(--color-text-1);
}
.folder-count {
margin-left: 4px;
color: var(--color-text-4);
}
}
.folder-text--active {
.folder-icon,
.folder-name,
.folder-count {
color: rgb(var(--primary-5));
}
}
}
.footer {
@apply flex items-center justify-between;
margin: auto -16px -16px;
padding: 12px 16px;
box-shadow: 0 -1px 4px 0 rgb(31 35 41 / 10%);
}
.expand-btn {
padding: 8px;
.arco-icon {
color: var(--color-text-4);
}
&:hover {
border-color: rgb(var(--primary-5)) !important;
background-color: rgb(var(--primary-1)) !important;
.arco-icon {
color: rgb(var(--primary-5));
}
}
}
:deep(.test-set-form-item) {
margin-bottom: 0;
.test-set {
border: 1px solid var(--color-text-n8);
border-right: none;
@apply flex items-center justify-center;
}
}
</style>

View File

@ -0,0 +1,20 @@
export default {
'ms.case.associate.title': 'Associated use cases',
'ms.case.associate.associate': 'associate',
'ms.case.associate.allCase': 'All use cases',
'ms.case.associate.caseName': 'Use case name',
'ms.case.associate.caseLevel': 'Use case levels',
'ms.case.associate.version': 'Version',
'ms.case.associate.versionPlaceholder': 'Default latest version',
'ms.case.associate.tags': 'Tag',
'ms.case.associate.searchPlaceholder': 'Search by ID or name',
'ms.case.associate.associateSuccess': 'Association successful',
'ms.case.associate.functionalCase': 'Functional use case',
'ms.case.associate.apiCase': 'Interface use case',
'ms.case.associate.apiScenarioCase': 'Interface scenario use case',
'ms.case.associate.UIScenario': 'UI scenario use case',
'ms.case.associate.performanceCase': 'Performance use case',
'ms.case.associate.testSet': 'Set of tests',
'ms.case.associate.addAssociatedCase': 'Add associated use case',
'ms.case.associate.automaticallyAddApiCase': 'Automatically adds associated interface use cases',
};

View File

@ -0,0 +1,20 @@
export default {
'ms.case.associate.title': '关联用例',
'ms.case.associate.associate': '关联',
'ms.case.associate.allCase': '全部用例',
'ms.case.associate.caseName': '用例名称',
'ms.case.associate.caseLevel': '用例等级',
'ms.case.associate.version': '版本',
'ms.case.associate.versionPlaceholder': '默认最新版本',
'ms.case.associate.tags': '标签',
'ms.case.associate.searchPlaceholder': '通过 ID 或名称搜索',
'ms.case.associate.associateSuccess': '关联成功',
'ms.case.associate.functionalCase': '功能用例',
'ms.case.associate.apiCase': '接口用例',
'ms.case.associate.apiScenarioCase': '接口场景用例',
'ms.case.associate.UIScenario': 'UI场景用例',
'ms.case.associate.performanceCase': '性能用例',
'ms.case.associate.testSet': '测试集',
'ms.case.associate.addAssociatedCase': '添加已关联用例',
'ms.case.associate.automaticallyAddApiCase': '自动添加已关联的接口用例',
};

View File

@ -0,0 +1,260 @@
<template>
<MsBaseTable
ref="tableRef"
class="mt-[16px]"
v-bind="propsRes"
:action-config="{
baseAction: [],
moreAction: [],
}"
v-on="propsEvent"
@filter-change="getModuleCount"
>
<template #num="{ record }">
<MsButton type="text">{{ record.num }}</MsButton>
</template>
<template #[FilterSlotNameEnum.CASE_MANAGEMENT_CASE_LEVEL]="{ filterContent }">
<CaseLevel :case-level="filterContent.value" />
</template>
<template #priority="{ record }">
<CaseLevel :case-level="record.priority" />
</template>
<template #[FilterSlotNameEnum.API_TEST_CASE_API_REPORT_EXECUTE_RESULT]="{ filterContent }">
<ExecutionStatus :module-type="ReportEnum.API_REPORT" :status="filterContent.value" />
</template>
<template #lastReportStatus="{ record }">
<ExecutionStatus
:module-type="ReportEnum.API_SCENARIO_REPORT"
:status="record.lastReportStatus ? record.lastReportStatus : 'PENDING'"
:script-identifier="record.scriptIdentifier"
/>
</template>
</MsBaseTable>
</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 { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import CaseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
import ExecutionStatus from '@/views/api-test/report/component/reportStatus.vue';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import type { TableQueryParams } from '@/models/common';
import { CaseLinkEnum } from '@/enums/caseEnum';
import { ReportEnum, ReportStatus } from '@/enums/reportEnum';
import { FilterRemoteMethodsEnum, FilterSlotNameEnum } from '@/enums/tableFilterEnum';
import { getPublicLinkCaseListMap } from './utils/page';
import { casePriorityOptions } from '@/views/api-test/components/config';
const { t } = useI18n();
const props = defineProps<{
associationType: string; // | |
modulesCount: Record<string, number>; //
activeModule: string;
offspringIds: string[];
currentProject: string;
associatedIds?: string[]; // ids
activeSourceType: keyof typeof CaseLinkEnum;
keyword: string;
}>();
const emit = defineEmits<{
(e: 'getModuleCount', params: TableQueryParams): void;
(e: 'refresh'): void;
(e: 'initModules'): void;
}>();
const appStore = useAppStore();
const statusList = computed(() => {
return Object.keys(ReportStatus).map((key) => {
return {
value: key,
label: t(ReportStatus[key].label),
};
});
});
const columns: MsTableColumn = [
{
title: 'ID',
dataIndex: 'num',
slotName: 'num',
sortIndex: 1,
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
fixed: 'left',
width: 160,
showTooltip: false,
columnSelectorDisabled: true,
},
{
title: 'apiScenario.table.columns.name',
dataIndex: 'name',
slotName: 'name',
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
width: 134,
showTooltip: true,
columnSelectorDisabled: true,
},
{
title: 'apiScenario.table.columns.level',
dataIndex: 'priority',
slotName: 'priority',
showDrag: true,
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
filterConfig: {
options: casePriorityOptions,
filterSlotName: FilterSlotNameEnum.CASE_MANAGEMENT_CASE_LEVEL,
},
width: 140,
},
{
title: 'apiScenario.table.columns.runResult',
dataIndex: 'lastReportStatus',
slotName: 'lastReportStatus',
showTooltip: false,
showDrag: true,
filterConfig: {
options: statusList.value,
filterSlotName: FilterSlotNameEnum.API_TEST_CASE_API_REPORT_EXECUTE_RESULT,
},
width: 200,
},
{
title: 'apiScenario.table.columns.passRate',
dataIndex: 'requestPassRate',
showDrag: true,
showInTable: false,
width: 100,
},
{
title: 'apiScenario.table.columns.createUser',
dataIndex: 'createUser',
slotName: 'createUserName',
showInTable: false,
showTooltip: true,
showDrag: true,
width: 109,
filterConfig: {
mode: 'remote',
loadOptionParams: {
projectId: appStore.currentProjectId,
},
remoteMethod: FilterRemoteMethodsEnum.PROJECT_PERMISSION_MEMBER,
placeholderText: t('caseManagement.featureCase.PleaseSelect'),
},
},
{
title: 'apiScenario.table.columns.tags',
dataIndex: 'tags',
isTag: true,
isStringTag: true,
showDrag: true,
},
];
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector, setPagination, resetFilterParams } =
useTable(undefined, {
columns,
showSetting: false,
selectable: true,
showSelectAll: true,
heightUsed: 310,
showSelectorAll: true,
});
async function getTableParams() {
return {
keyword: props.keyword,
projectId: props.currentProject,
moduleIds: props.activeModule === 'all' || !props.activeModule ? [] : [props.activeModule, ...props.offspringIds],
excludeIds: [...(props.associatedIds || [])], // id
condition: {
keyword: props.keyword,
},
};
}
async function getModuleCount() {
const tableParams = await getTableParams();
emit('getModuleCount', {
...tableParams,
current: propsRes.value.msPagination?.current,
pageSize: propsRes.value.msPagination?.pageSize,
});
}
async function loadScenarioList() {
const tableParams = await getTableParams();
setLoadListParams(tableParams);
loadList();
emit('getModuleCount', {
...tableParams,
current: propsRes.value.msPagination?.current,
pageSize: propsRes.value.msPagination?.pageSize,
});
}
const tableRef = ref<InstanceType<typeof MsBaseTable>>();
watch(
() => props.activeSourceType,
(val) => {
if (val) {
tableRef.value?.initColumn(columns);
resetSelector();
resetFilterParams();
setPagination({
current: 1,
});
}
}
);
watch(
() => props.currentProject,
(val) => {
if (val) {
loadScenarioList();
}
},
{
immediate: true,
}
);
function getScenarioSaveParams() {
const { excludeKeys, selectedKeys, selectorStatus } = propsRes.value;
const tableParams = getTableParams();
return {
...tableParams,
excludeIds: [...excludeKeys].concat(...(props.associatedIds || [])),
selectIds: selectorStatus === 'all' ? [] : [...selectedKeys],
selectAll: selectorStatus === 'all',
};
}
defineExpose({
getScenarioSaveParams,
loadScenarioList,
});
</script>
<style scoped></style>

View File

@ -0,0 +1,31 @@
import { getModuleCount } from '@/api/modules/api-test/management';
import { getModuleCount as getScenarioModuleCount } from '@/api/modules/api-test/scenario';
import { getCaseModulesCounts } from '@/api/modules/case-management/featureCase';
import { CaseCountApiTypeEnum } from '@/enums/associateCaseEnum';
import { CaseLinkEnum } from '@/enums/caseEnum';
// 获取模块数量Map
export const getModuleTreeCountApiMap: Record<string, any> = {
[CaseCountApiTypeEnum.TEST_PLAN_CASE_COUNT]: {
[CaseLinkEnum.FUNCTIONAL]: getCaseModulesCounts,
[CaseLinkEnum.API]: getModuleCount,
[CaseLinkEnum.SCENARIO]: getScenarioModuleCount,
},
};
// 获取模块count
export function initGetModuleCountFunc(
type: CaseCountApiTypeEnum[keyof CaseCountApiTypeEnum],
activeTab: keyof typeof CaseLinkEnum,
params: Record<string, any>
) {
switch (type) {
case CaseCountApiTypeEnum.TEST_PLAN_CASE_COUNT:
return getModuleTreeCountApiMap[type][activeTab](params);
default:
break;
}
}
export default {};

View File

@ -0,0 +1,31 @@
import { getModuleTreeOnlyModules } from '@/api/modules/api-test/management';
import { getModuleTree as getScenarioModuleTree } from '@/api/modules/api-test/scenario';
import { getCaseModuleTree } from '@/api/modules/case-management/featureCase';
import { CaseModulesApiTypeEnum } from '@/enums/associateCaseEnum';
import { CaseLinkEnum } from '@/enums/caseEnum';
// 模块树接口
export const getModuleTreeApiMap: Record<string, any> = {
[CaseModulesApiTypeEnum.TEST_PLAN_LINK_CASE_MODULE]: {
[CaseLinkEnum.FUNCTIONAL]: getCaseModuleTree,
[CaseLinkEnum.API]: getModuleTreeOnlyModules,
[CaseLinkEnum.SCENARIO]: getScenarioModuleTree,
},
};
// 获取关联用例模块
export function getModuleTreeFunc(
getModulesApiType: CaseModulesApiTypeEnum[keyof CaseModulesApiTypeEnum],
activeTab: keyof typeof CaseLinkEnum,
params: Record<string, any>
) {
switch (getModulesApiType) {
case CaseModulesApiTypeEnum.TEST_PLAN_LINK_CASE_MODULE:
return getModuleTreeApiMap[getModulesApiType][activeTab](params);
default:
break;
}
}
export default {};

View File

@ -0,0 +1,37 @@
import { getUnAssociatedList } from '@/api/modules/bug-management';
import { getCaseList, getPublicLinkCaseList } from '@/api/modules/case-management/featureCase';
import {
getTestPlanAssociationApiList,
getTestPlanAssociationCaseList,
getTestPlanCaseList,
} from '@/api/modules/test-plan/testPlan';
import { CasePageApiTypeEnum } from '@/enums/associateCaseEnum';
import { CaseLinkEnum } from '@/enums/caseEnum';
// table接口模块定义
export const getPublicLinkCaseListMap: Record<string, any> = {
// 功能用例 目前只有接口用例、场景用例
[CasePageApiTypeEnum.FUNCTIONAL_CASE_PAGE]: {
[CaseLinkEnum.API]: getPublicLinkCaseList,
[CaseLinkEnum.SCENARIO]: getPublicLinkCaseList,
},
// 用例评审 目前只有功能用例
[CasePageApiTypeEnum.CASE_REVIEW_CASE_PAGE]: {
[CaseLinkEnum.FUNCTIONAL]: getCaseList,
},
// 缺陷管理 目前只有功能用例
[CasePageApiTypeEnum.BUG_MANAGEMENT_CASE_PAGE]: {
[CaseLinkEnum.FUNCTIONAL]: getUnAssociatedList,
},
// 测试计划 目前有功能用例、接口用例、场景用例
[CasePageApiTypeEnum.TEST_PLAN_CASE_PAGE]: {
[CaseLinkEnum.FUNCTIONAL]: getTestPlanCaseList,
[CaseLinkEnum.API]: {
API: getTestPlanAssociationApiList,
CASE: getTestPlanAssociationCaseList,
},
},
};
export default {};

View File

@ -0,0 +1,20 @@
export enum CaseModulesApiTypeEnum {
FUNCTIONAL_CASE_MODULE = 'FUNCTIONAL_CASE_MODULE', // 功能用例关联模块树
BUG_MANAGEMENT_MODULE = 'BUG_MANAGEMENT_MODULE', // 缺陷管理关联模块树
CASE_MANAGEMENT_MODULE = 'CASE_MANAGEMENT_MODULE', // 用例评审关联用例模块树
TEST_PLAN_LINK_CASE_MODULE = 'TEST_PLAN_LINK_CASE_MODULE', // 测试计划关联用例模块树
}
export enum CasePageApiTypeEnum {
FUNCTIONAL_CASE_PAGE = 'FUNCTIONAL_CASE_PAGE', // 功能用例关联用例分页
BUG_MANAGEMENT_CASE_PAGE = 'BUG_MANAGEMENT_CASE_PAGE', // 缺陷管理关联用例分页
CASE_REVIEW_CASE_PAGE = 'CASE_REVIEW_CASE_PAGE', // 用例评审关联用例分页
TEST_PLAN_CASE_PAGE = 'TEST_PLAN_CASE_PAGE', // 测试计划关联用例分页
}
export enum CaseCountApiTypeEnum {
FUNCTIONAL_CASE_COUNT = 'FUNCTIONAL_CASE_COUNT', // 功能用例关联用例模块数量
BUG_MANAGEMENT_CASE_COUNT = 'BUG_MANAGEMENT_CASE_COUNT', // 缺陷管理关联用例模块数量
CASE_MANAGEMENT_CASE_COUNT = 'CASE_MANAGEMENT_CASE_COUNT', // 用例评审关联用例模块数量
TEST_PLAN_CASE_COUNT = 'TEST_PLAN_CASE_COUNT', // 测试计划关联用例模块数量
}
export default {};

View File

@ -185,5 +185,6 @@ export default {
'common.updateTime': 'Update time',
'common.belongProject': 'Belong to Project',
'common.noMatchData': 'No matching data',
'common.name': 'name',
'common.stopped': 'Stopped',
};

View File

@ -186,5 +186,6 @@ export default {
'common.updateTime': '更新时间',
'common.belongProject': '所属项目',
'common.noMatchData': '暂无匹配数据',
'common.name': '名称',
'common.stopped': '已停止',
};

View File

@ -4,6 +4,7 @@ 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';
export type planStatusType = 'PREPARED' | 'UNDERWAY' | 'COMPLETED' | 'ARCHIVED';
@ -76,12 +77,12 @@ export interface TestPlanDetail extends AddTestPlanParams {
// 计划分页
export interface TestPlanItem {
id?: string;
id: string;
projectId: string;
num: number;
name: string;
status: planStatusType;
type: string;
type: keyof typeof testPlanTypeEnum;
tags: string[];
schedule: string; // 是否定时
createUser: string;
@ -91,6 +92,7 @@ export interface TestPlanItem {
children: TestPlanItem[];
childrenCount: number;
groupId: string;
functionalCaseCount: number;
}
export type TestPlanItemType = TestPlanItem & TestPlanDetail;
@ -236,6 +238,16 @@ export interface ExecuteHistoryItem {
deleted: boolean;
}
export interface moduleForm {
moveType: 'MODULE' | 'GROUP';
targetId: string | number;
}
export interface BatchMoveParams extends TableQueryParams {
moveType?: 'MODULE' | 'GROUP';
targetId?: string | number;
}
// TODO: 联调
export interface PlanDetailApiCaseItem {
id: string;

View File

@ -36,7 +36,7 @@ export default {
'report.detail.api.requestTotalTimeTip': 'The total response time of all requests',
'report.detail.api.assertPass': 'Assert pass',
'report.detail.api.executionRate': 'Req execution Rate',
'report.detail.api.requestAnalysis': 'Request analysis',
'report.detail.api.requestAnalysis': 'Report analysis',
'report.detail.api.total': 'total',
'report.detail.api.reportDetail': 'Report detail',
'report.detail.api.filterPlaceholder': 'Please select a filter conditions',

View File

@ -34,7 +34,7 @@ export default {
'report.detail.api.requestTotalTimeTip': '全部请求的响应时间总和',
'report.detail.api.assertPass': '断言通过率',
'report.detail.api.executionRate': '请求执行率',
'report.detail.api.requestAnalysis': '请求分析',
'report.detail.api.requestAnalysis': '报告分析',
'report.detail.api.total': '总数(个)',
'report.detail.api.reportDetail': '报告明细',
'report.detail.api.filterPlaceholder': '请选择过滤条件',

View File

@ -53,6 +53,7 @@
label: 'common.fakeError',
},
DEFAULT: {
icon: '',
label: '-',
color: '!text-[var(--color-text-input-border)]',
},

View File

@ -61,7 +61,7 @@
</template>
<!-- 执行状态筛选 -->
<template #resultStatus="{ record }">
<ExecutionStatus :status="record.resultStatus" />
<ExecutionStatus v-if="record.resultStatus !== '-'" :status="record.resultStatus" />
</template>
<template #execStatus="{ record }">
<ExecStatus :status="record.execStatus" />

View File

@ -1,6 +1,6 @@
<template>
<div class="flex items-center justify-start">
<MsIcon :type="getExecutionResult().icon" :class="getExecutionResult()?.color" size="14" />
<MsIcon :type="getExecutionResult()?.icon" :class="getExecutionResult()?.color" size="14" />
<span class="ml-1">{{ t(getExecutionResult().label) }}</span>
</div>
</template>

View File

@ -0,0 +1,162 @@
<template>
<MsBaseTable v-bind="propsRes" v-on="propsEvent">
<template #num="{ record }">
<MsButton type="text">{{ record.num }}</MsButton>
</template>
<template #[FilterSlotNameEnum.CASE_MANAGEMENT_CASE_LEVEL]="{ filterContent }">
<CaseLevel :case-level="filterContent.value" />
</template>
<template #caseLevel="{ record }">
<CaseLevel :case-level="record.caseLevel" />
</template>
<template #[FilterSlotNameEnum.CASE_MANAGEMENT_EXECUTE_RESULT]="{ filterContent }">
<ExecuteResult :execute-result="filterContent.key" />
</template>
<template #lastExecResult="{ record }">
<ExecuteResult :execute-result="record.lastExecResult" />
<MsIcon
v-show="record.lastExecResult !== LastExecuteResults.PENDING"
type="icon-icon_take-action_outlined"
class="ml-[8px] cursor-pointer text-[rgb(var(--primary-5))]"
size="16"
@click="showReport(record)"
/>
</template>
</MsBaseTable>
</template>
<script setup lang="ts">
import { onBeforeMount } from '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 { getReportBugList, getReportShareBugList } from '@/api/modules/test-plan/report';
import { getPlanDetailApiCaseList } from '@/api/modules/test-plan/testPlan';
import { useTableStore } from '@/store';
import type { PlanDetailApiScenarioItem } from '@/models/testPlan/testPlan';
import { LastExecuteResults } from '@/enums/caseEnum';
import { TableKeyEnum } from '@/enums/tableEnum';
import { FilterSlotNameEnum } from '@/enums/tableFilterEnum';
import { casePriorityOptions } from '@/views/api-test/components/config';
import { executionResultMap, getCaseLevels } from '@/views/case-management/caseManagementFeature/components/utils';
const props = defineProps<{
reportId: string;
shareId?: string;
}>();
const tableStore = useTableStore();
const columns: MsTableColumn = [
{
title: 'ID',
dataIndex: 'num',
sortIndex: 1,
fixed: 'left',
width: 100,
showTooltip: true,
},
{
title: 'common.name',
dataIndex: 'name',
width: 150,
showTooltip: true,
},
{
title: 'report.detail.level',
dataIndex: 'caseLevel',
slotName: 'caseLevel',
filterConfig: {
options: casePriorityOptions,
filterSlotName: FilterSlotNameEnum.CASE_MANAGEMENT_CASE_LEVEL,
},
width: 150,
showDrag: true,
},
{
title: 'common.executionResult',
dataIndex: 'lastExecResult',
slotName: 'lastExecResult',
filterConfig: {
valueKey: 'key',
labelKey: 'statusText',
options: Object.values(executionResultMap),
filterSlotName: FilterSlotNameEnum.CASE_MANAGEMENT_EXECUTE_RESULT,
},
width: 150,
showDrag: true,
},
{
title: 'common.belongModule',
dataIndex: 'moduleId',
showTooltip: true,
width: 200,
showDrag: true,
},
{
title: 'case.tableColumnCreateUser',
dataIndex: 'createUserName',
showTooltip: true,
width: 130,
showDrag: true,
},
{
title: 'testPlan.featureCase.executor',
dataIndex: 'executeUserName',
showTooltip: true,
width: 130,
showDrag: true,
},
{
title: 'testPlan.featureCase.executor',
dataIndex: 'executeUserName',
showTooltip: true,
width: 130,
showDrag: true,
},
{
title: 'testPlan.featureCase.bugCount',
dataIndex: 'bugCount',
slotName: 'bugCount',
width: 100,
showDrag: true,
},
];
const reportBugList = () => {
return !props.shareId ? getPlanDetailApiCaseList : getReportShareBugList;
};
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(getPlanDetailApiCaseList, {
scroll: { x: '100%' },
columns,
tableKey: TableKeyEnum.TEST_PLAN_REPORT_DETAIL_BUG,
showSelectorAll: false,
});
async function loadCaseList() {
setLoadListParams({ reportId: props.reportId, shareId: props.shareId ?? undefined });
loadList();
}
watchEffect(() => {
if (props.reportId) {
loadCaseList();
}
});
//
const reportVisible = ref(false);
const apiReportId = ref('');
function showReport(record: PlanDetailApiScenarioItem) {
reportVisible.value = true;
apiReportId.value = record.lastExecResultReportId; // TODO
}
await tableStore.initColumn(TableKeyEnum.TEST_PLAN_REPORT_DETAIL_BUG, columns, 'drawer');
</script>

View File

@ -29,6 +29,9 @@
:request-total="getIndicators(detail.caseTotal) || 0"
/>
</div>
</div>
<!-- TODO 接口用例&场景用例待联调 -->
<div class="analysis-wrapper">
<div class="analysis min-w-[330px]">
<div class="block-title">{{ t('report.detail.useCaseAnalysis') }}</div>
<div class="flex">
@ -59,6 +62,66 @@
</div>
</div>
</div>
<div class="analysis min-w-[330px]">
<div class="block-title">{{ t('report.detail.apiUseCaseAnalysis') }}</div>
<div class="flex">
<div class="w-[70%]">
<SingleStatusProgress :detail="detail" status="pending" />
<SingleStatusProgress :detail="detail" status="success" />
<SingleStatusProgress :detail="detail" status="block" />
<SingleStatusProgress :detail="detail" status="error" />
</div>
<div class="relative w-[30%] min-w-[150px]">
<div class="charts absolute w-full text-center">
<div class="text-[12px] !text-[var(--color-text-4)]">{{ t('report.passRate') }}</div>
<a-popover position="bottom" content-class="response-popover-content">
<div class="flex justify-center text-[18px] font-medium">
<div class="one-line-text max-w-[80px] text-[var(--color-text-1)]">{{ functionCasePassRate }} </div>
</div>
<template #content>
<div class="min-w-[95px] max-w-[400px] p-4 text-[14px]">
<div class="text-[12px] font-medium text-[var(--color-text-4)]">{{ t('report.passRate') }}</div>
<div class="mt-2 text-[18px] font-medium text-[var(--color-text-1)]">{{ functionCasePassRate }}</div>
</div>
</template>
</a-popover>
</div>
<div class="flex h-full w-full min-w-[150px] items-center justify-center">
<MsChart width="150px" height="150px" :options="functionCaseOptions"
/></div>
</div>
</div>
</div>
<div class="analysis min-w-[330px]">
<div class="block-title">{{ t('report.detail.scenarioUseCaseAnalysis') }}</div>
<div class="flex">
<div class="w-[70%]">
<SingleStatusProgress :detail="detail" status="pending" />
<SingleStatusProgress :detail="detail" status="success" />
<SingleStatusProgress :detail="detail" status="block" />
<SingleStatusProgress :detail="detail" status="error" />
</div>
<div class="relative w-[30%] min-w-[150px]">
<div class="charts absolute w-full text-center">
<div class="text-[12px] !text-[var(--color-text-4)]">{{ t('report.passRate') }}</div>
<a-popover position="bottom" content-class="response-popover-content">
<div class="flex justify-center text-[18px] font-medium">
<div class="one-line-text max-w-[80px] text-[var(--color-text-1)]">{{ functionCasePassRate }} </div>
</div>
<template #content>
<div class="min-w-[95px] max-w-[400px] p-4 text-[14px]">
<div class="text-[12px] font-medium text-[var(--color-text-4)]">{{ t('report.passRate') }}</div>
<div class="mt-2 text-[18px] font-medium text-[var(--color-text-1)]">{{ functionCasePassRate }}</div>
</div>
</template>
</a-popover>
</div>
<div class="flex h-full w-full min-w-[150px] items-center justify-center">
<MsChart width="150px" height="150px" :options="functionCaseOptions"
/></div>
</div>
</div>
</div>
</div>
<MsCard class="mb-[16px]" simple auto-height auto-width>
<div class="font-medium">{{ t('report.detail.reportSummary') }}</div>
@ -72,7 +135,14 @@
:preview-url="PreviewEditorImageUrl"
class="mt-[8px] w-full"
:editable="!!shareId"
/></div>
/>
<MsFormItemSub
v-if="hasAnyPermission(['PROJECT_TEST_PLAN_REPORT:READ+UPDATE']) && !shareId && showButton"
:text="t('report.detail.oneClickSummary')"
:show-fill-icon="true"
@fill="handleSummary"
/>
</div>
<div
v-show="showButton && hasAnyPermission(['PROJECT_TEST_PLAN_REPORT:READ+UPDATE']) && !shareId"
@ -92,6 +162,8 @@
/>
<BugTable v-if="activeTab === 'bug'" :report-id="detail.id" :share-id="shareId" />
<FeatureCaseTable v-if="activeTab === 'featureCase'" :report-id="detail.id" :share-id="shareId" />
<ApiCaseTable v-if="activeTab === 'apiCase'" :report-id="detail.id" :share-id="shareId" />
<ScenarioCaseTable v-if="activeTab === 'scenarioCase'" :report-id="detail.id" :share-id="shareId" />
</MsCard>
</template>
@ -106,12 +178,15 @@
import MsCard from '@/components/pure/ms-card/index.vue';
import MsRichText from '@/components/pure/ms-rich-text/MsRichText.vue';
import MsTab from '@/components/pure/ms-tab/index.vue';
import MsFormItemSub from '@/components/business/ms-form-item-sub/index.vue';
import PlanDetailHeaderRight from './planDetailHeaderRight.vue';
import ReportMetricsItem from './ReportMetricsItem.vue';
import SetReportChart from '@/views/api-test/report/component/case/setReportChart.vue';
import SingleStatusProgress from '@/views/test-plan/report/component/singleStatusProgress.vue';
import ApiCaseTable from '@/views/test-plan/report/detail/component/apiCaseTable.vue';
import BugTable from '@/views/test-plan/report/detail/component/bugTable.vue';
import FeatureCaseTable from '@/views/test-plan/report/detail/component/featureCaseTable.vue';
import ScenarioCaseTable from '@/views/test-plan/report/detail/component/scenarioCaseTable.vue';
import { editorUploadFile, updateReportDetail } from '@/api/modules/test-plan/report';
import { PreviewEditorImageUrl } from '@/api/requrls/case-management/featureCase';
@ -369,6 +444,14 @@
value: 'featureCase',
label: t('report.detail.featureCaseDetails'),
},
{
value: 'apiCase',
label: t('report.detail.apiCaseDetails'),
},
{
value: 'scenarioCase',
label: t('report.detail.scenarioCaseDetails'),
},
]);
watchEffect(() => {
@ -387,6 +470,14 @@
});
});
});
const summaryContent = ref<string>(`
<p style=""><span color="" fontsize="">本次完成 测试计划名称功能测试接口测试 300 用例已执行 285 未执行 15 执行率为 95%通过用例 270 通过率为 90%达到/未达到通过阈值通过阈值为85%xxx计划满足/不满足发布要求<br>1本次测试包含100条功能测试用例执行了95条未执行5条执行率为95%通过用例90条通过率为90%共发现缺陷0个<br>2本次测试包含100条接口测试用例执行了95条未执行5条执行率为95%通过用例90条通过率为90%共发现缺陷0个<br>3本次测试包含100条场景测试用例执行了95条未执行5条执行率为95%通过用例90条通过率为90%共发现缺陷0个</span></p>
`);
// TODO
function handleSummary() {
richText.value.summary = summaryContent.value;
}
</script>
<style scoped lang="less">
@ -410,7 +501,4 @@
}
}
}
:deep(.rich-wrapper) .halo-rich-text-editor .ProseMirror {
height: 58px;
}
</style>

View File

@ -0,0 +1,153 @@
<template>
<MsBaseTable v-bind="propsRes" v-on="propsEvent">
<template #num="{ record }">
<MsButton type="text">{{ record.num }}</MsButton>
</template>
<template #[FilterSlotNameEnum.CASE_MANAGEMENT_CASE_LEVEL]="{ filterContent }">
<CaseLevel :case-level="filterContent.value" />
</template>
<template #caseLevel="{ record }">
<CaseLevel :case-level="record.caseLevel" />
</template>
<template #[FilterSlotNameEnum.CASE_MANAGEMENT_EXECUTE_RESULT]="{ filterContent }">
<ExecuteResult :execute-result="filterContent.key" />
</template>
<template #lastExecResult="{ record }">
<ExecuteResult :execute-result="record.lastExecResult" />
<MsIcon
v-show="record.lastExecResult !== LastExecuteResults.PENDING"
type="icon-icon_take-action_outlined"
class="ml-[8px] cursor-pointer text-[rgb(var(--primary-5))]"
size="16"
@click="showReport(record)"
/>
</template>
</MsBaseTable>
</template>
<script setup lang="ts">
import { onBeforeMount } from '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 { getReportBugList, getReportShareBugList } from '@/api/modules/test-plan/report';
import { getPlanDetailApiCaseList } from '@/api/modules/test-plan/testPlan';
import { useTableStore } from '@/store';
import type { PlanDetailApiScenarioItem } from '@/models/testPlan/testPlan';
import { LastExecuteResults } from '@/enums/caseEnum';
import { TableKeyEnum } from '@/enums/tableEnum';
import { FilterSlotNameEnum } from '@/enums/tableFilterEnum';
import { casePriorityOptions } from '@/views/api-test/components/config';
import { executionResultMap, getCaseLevels } from '@/views/case-management/caseManagementFeature/components/utils';
const props = defineProps<{
reportId: string;
shareId?: string;
}>();
const tableStore = useTableStore();
const columns: MsTableColumn = [
{
title: 'ID',
dataIndex: 'num',
sortIndex: 1,
fixed: 'left',
width: 100,
showTooltip: true,
},
{
title: 'common.name',
dataIndex: 'name',
width: 150,
showTooltip: true,
},
{
title: 'report.detail.level',
dataIndex: 'caseLevel',
slotName: 'caseLevel',
filterConfig: {
options: casePriorityOptions,
filterSlotName: FilterSlotNameEnum.CASE_MANAGEMENT_CASE_LEVEL,
},
width: 150,
showDrag: true,
},
{
title: 'common.executionResult',
dataIndex: 'lastExecResult',
slotName: 'lastExecResult',
filterConfig: {
valueKey: 'key',
labelKey: 'statusText',
options: Object.values(executionResultMap),
filterSlotName: FilterSlotNameEnum.CASE_MANAGEMENT_EXECUTE_RESULT,
},
width: 150,
showDrag: true,
},
{
title: 'common.belongModule',
dataIndex: 'moduleId',
showTooltip: true,
width: 200,
showDrag: true,
},
{
title: 'case.tableColumnCreateUser',
dataIndex: 'createUserName',
showTooltip: true,
width: 130,
showDrag: true,
},
{
title: 'testPlan.featureCase.executor',
dataIndex: 'executeUserName',
showTooltip: true,
width: 130,
showDrag: true,
},
{
title: 'testPlan.featureCase.bugCount',
dataIndex: 'bugCount',
slotName: 'bugCount',
width: 100,
showDrag: true,
},
];
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(getPlanDetailApiCaseList, {
scroll: { x: '100%' },
columns,
tableKey: TableKeyEnum.TEST_PLAN_REPORT_DETAIL_BUG,
showSelectorAll: false,
});
async function loadCaseList() {
setLoadListParams({ reportId: props.reportId, shareId: props.shareId ?? undefined });
loadList();
}
watchEffect(() => {
if (props.reportId) {
loadCaseList();
}
});
//
const reportVisible = ref(false);
const apiReportId = ref('');
function showReport(record: PlanDetailApiScenarioItem) {
reportVisible.value = true;
apiReportId.value = record.lastExecResultReportId; // TODO
}
await tableStore.initColumn(TableKeyEnum.TEST_PLAN_REPORT_DETAIL_BUG, columns, 'drawer');
</script>

View File

@ -33,5 +33,11 @@ export default {
'report.detail.performCompletion': 'Perform completion',
'report.detail.totalDefects': 'Total defects',
'report.detail.useCaseAnalysis': 'Function of use case analysis',
'report.detail.apiUseCaseAnalysis': 'Api use case analysis',
'report.detail.scenarioUseCaseAnalysis': 'Scenario use case analysis',
'report.detail.number': 'number',
'report.detail.level': 'level',
'report.detail.apiCaseDetails': 'Api use case details',
'report.detail.scenarioCaseDetails': 'Scenario use case details',
'report.detail.oneClickSummary': 'One click report summary',
};

View File

@ -33,5 +33,11 @@ export default {
'report.detail.performCompletion': '执行完成率',
'report.detail.totalDefects': '缺陷总数',
'report.detail.useCaseAnalysis': '功能用例分析',
'report.detail.apiUseCaseAnalysis': '接口用例分析',
'report.detail.scenarioUseCaseAnalysis': '场景用例分析',
'report.detail.number': '个',
'report.detail.level': '等级',
'report.detail.apiCaseDetails': '接口用例明细',
'report.detail.scenarioCaseDetails': '场景用例明细',
'report.detail.oneClickSummary': '一键填写报告总结',
};

View File

@ -49,12 +49,12 @@
import { characterLimit } from '@/utils';
import type { TestPlanDetail, TestPlanItem } from '@/models/testPlan/testPlan';
import { testPlanTypeEnum } from '@/enums/testPlanEnum';
const { t } = useI18n();
const props = defineProps<{
visible: boolean;
// isScheduled: boolean; // TODO
record: TestPlanItem | TestPlanDetail | undefined; // record
}>();
@ -71,16 +71,16 @@
const confirmLoading = ref<boolean>(false);
//
async function confirmHandler(isDelete: boolean) {
try {
confirmLoading.value = true;
if (isDelete) {
await deletePlan(props.record?.id);
emit('success', true);
} else {
await archivedPlan(props.record?.id);
emit('success', false);
}
emit('success', isDelete);
Message.success(isDelete ? t('common.deleteSuccess') : t('common.batchArchiveSuccess'));
showModalVisible.value = false;
} catch (error) {
@ -91,6 +91,9 @@
}
const contentTip = computed(() => {
if (props.record?.type === testPlanTypeEnum.GROUP) {
return t('testPlan.testPlanGroup.planGroupDeleteContent');
}
switch (props.record && props.record.status) {
case 'ARCHIVED':
return t('testPlan.testPlanIndex.deleteArchivedPlan');

View File

@ -1,19 +1,15 @@
<template>
<MsCaseAssociate
v-model:visible="innerVisible"
v-model:currentSelectCase="currentSelectCase"
:get-modules-func="getCaseModuleTree"
:get-table-func="getTestPlanCaseList"
v-model:project-id="currentProjectId"
:get-modules-api-type="CaseModulesApiTypeEnum.TEST_PLAN_LINK_CASE_MODULE"
:get-page-api-type="CasePageApiTypeEnum.TEST_PLAN_CASE_PAGE"
:get-module-count-api-type="CaseCountApiTypeEnum.TEST_PLAN_CASE_COUNT"
:confirm-loading="confirmLoading"
:table-params="{
:extra-table-params="{
testPlanId: props?.testPlanId,
}"
:associated-ids="props.hasNotAssociatedIds || []"
:project-id="currentProjectId"
:type="RequestModuleEnum.CASE_MANAGEMENT"
hide-project-select
:is-hidden-case-level="false"
:selector-all="true"
@save="saveHandler"
>
</MsCaseAssociate>
@ -23,15 +19,13 @@
import { useRoute } from 'vue-router';
import { Message } from '@arco-design/web-vue';
import MsCaseAssociate from '@/components/business/ms-case-associate/index.vue';
import { RequestModuleEnum } from '@/components/business/ms-case-associate/utils';
import MsCaseAssociate from '@/components/business/ms-associate-case/index.vue';
import { getCaseModuleTree } from '@/api/modules/case-management/featureCase';
import { getTestPlanCaseList } from '@/api/modules/test-plan/testPlan';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import type { AssociateCaseRequest, AssociateCaseRequestType } from '@/models/testPlan/testPlan';
import { CaseCountApiTypeEnum, CaseModulesApiTypeEnum, CasePageApiTypeEnum } from '@/enums/associateCaseEnum';
import { CaseLinkEnum } from '@/enums/caseEnum';
const { t } = useI18n();
@ -49,7 +43,6 @@
const appStore = useAppStore();
const route = useRoute();
const currentSelectCase = ref<keyof typeof CaseLinkEnum>('FUNCTIONAL');
const currentProjectId = ref(appStore.currentProjectId);
const confirmLoading = ref<boolean>(false);

View File

@ -16,48 +16,78 @@
</div>
</div>
</template>
<a-input
v-model:model-value="moduleKeyword"
:placeholder="t('caseManagement.caseReview.folderSearchPlaceholder')"
allow-clear
:max-length="255"
class="mb-4"
/>
<a-spin class="min-h-[400px] w-full" :loading="loading">
<MsTree
v-model:focus-node-key="focusNodeKey"
v-model:selected-keys="innerSelectedModuleKeys"
:data="treeData"
:keyword="moduleKeyword"
:default-expand-all="props.isExpandAll"
:expand-all="isExpandAll"
:empty-text="t(props.emptyText)"
:draggable="false"
:virtual-list-props="virtualListProps"
:field-names="{
title: 'name',
key: 'id',
children: 'children',
count: 'count',
}"
block-node
title-tooltip-position="top"
@select="nodeSelect"
<div v-if="props.type === testPlanTypeEnum.TEST_PLAN" class="mb-[16px] flex items-center">
<span class="mr-2 text-[var(--color-text-1)]"
>{{ props.mode === 'move' ? t('msTable.batch.moveTo') : t('msTable.batch.copyTo') }}:
</span>
<a-radio-group v-model="form.moveType" class="file-show-type mr-2">
<a-radio value="MODULE" class="show-type-icon p-[2px]">{{ t('testPlan.testPlanGroup.module') }}</a-radio>
<a-radio value="GROUP" class="show-type-icon p-[2px]">{{ t('testPlan.testPlanIndex.testPlanGroup') }}</a-radio>
</a-radio-group>
</div>
<a-form
v-if="form.moveType === 'GROUP' && props.type === testPlanTypeEnum.TEST_PLAN"
ref="formRef"
:model="form"
layout="vertical"
class="flex items-center"
>
<a-form-item
:rules="[{ required: true, message: t('testPlan.testPlanGroup.selectTestPlanGroupPlaceHolder') }]"
field="targetId"
:label="t('testPlan.testPlanIndex.testPlanGroup')"
>
<template #title="nodeData">
<div class="inline-flex w-full">
<div class="one-line-text w-full text-[var(--color-text-1)]">{{ nodeData.name }}</div>
</div>
</template>
</MsTree>
</a-spin>
<a-select v-model="form.targetId" :placeholder="t('common.pleaseSelect')">
<a-option v-for="item of groupList" :key="item.id" :value="item.id">
{{ item.name }}
</a-option>
</a-select>
</a-form-item>
</a-form>
<div v-if="form.moveType === 'MODULE'">
<a-input
v-model:model-value="moduleKeyword"
:placeholder="t('caseManagement.caseReview.folderSearchPlaceholder')"
allow-clear
:max-length="255"
class="mb-4"
/>
<a-spin class="min-h-[300px] w-full" :loading="loading">
<MsTree
v-model:focus-node-key="focusNodeKey"
v-model:selected-keys="innerSelectedModuleKeys"
:data="treeData"
:keyword="moduleKeyword"
:default-expand-all="props.isExpandAll"
:expand-all="isExpandAll"
:empty-text="t(props.emptyText)"
:draggable="false"
:virtual-list-props="virtualListProps"
:field-names="{
title: 'name',
key: 'id',
children: 'children',
count: 'count',
}"
block-node
title-tooltip-position="top"
@select="nodeSelect"
>
<template #title="nodeData">
<div class="inline-flex w-full">
<div class="one-line-text w-full text-[var(--color-text-1)]">{{ nodeData.name }}</div>
</div>
</template>
</MsTree>
</a-spin>
</div>
<template #footer>
<a-button type="secondary" @click="handleMoveCaseModalCancel">{{ t('common.cancel') }}</a-button>
<a-button
class="ml-[12px]"
type="primary"
:loading="props.okLoading"
:disabled="innerSelectedModuleKeys.length === 0"
:disabled="innerSelectedModuleKeys.length === 0 && form.moveType === 'MODULE'"
@click="handleCaseMoveOrCopy"
>
{{ props.mode === 'move' ? t('common.move') : t('common.copy') }}
@ -69,16 +99,22 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useVModel } from '@vueuse/core';
import { SelectOptionData } from '@arco-design/web-vue';
import MsTree from '@/components/business/ms-tree/index.vue';
import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
import { getPlanGroupOptions } from '@/api/modules/test-plan/testPlan';
import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store';
import { mapTree } from '@/utils';
import type { TableQueryParams } from '@/models/common';
import { ModuleTreeNode } from '@/models/common';
import type { moduleForm } from '@/models/testPlan/testPlan';
import { testPlanTypeEnum } from '@/enums/testPlanEnum';
import type { FormInstance, ValidatedError } from '@arco-design/web-vue';
const appStore = useAppStore();
const { t } = useI18n();
@ -92,6 +128,7 @@
selectedNodeKeys: (string | number)[];
okLoading: boolean;
emptyText?: string;
type: keyof typeof testPlanTypeEnum;
}>(),
{
isExpandAll: false,
@ -102,7 +139,7 @@
const emit = defineEmits<{
(e: 'update:visible', val: boolean): void;
(e: 'update:selectedNodeKeys', val: string[]): void;
(e: 'save'): void;
(e: 'save', form: moduleForm): void;
}>();
const showModalVisible = useVModel(props, 'visible', emit);
@ -110,17 +147,36 @@
const moduleKeyword = ref<string>('');
const focusNodeKey = ref<string>('');
const form = ref<moduleForm>({
moveType: 'MODULE',
targetId: '',
});
const groupList = ref<SelectOptionData>([]);
const focusNodeKey = ref<string>('');
const formRef = ref<FormInstance | null>(null);
//
async function handleCaseMoveOrCopy() {
emit('save');
if (form.value.moveType === 'GROUP') {
formRef.value?.validate(async (errors: undefined | Record<string, ValidatedError>) => {
if (!errors) {
emit('save', form.value);
}
});
} else {
emit('save', form.value);
}
}
function handleMoveCaseModalCancel() {
showModalVisible.value = false;
innerSelectedModuleKeys.value = [];
moduleKeyword.value = '';
form.value = {
moveType: 'MODULE',
targetId: '',
};
}
const loading = ref<boolean>(false);
@ -173,11 +229,21 @@
innerSelectedModuleKeys.value = selectedKeys;
};
async function initGroupOptions() {
try {
groupList.value = await getPlanGroupOptions(appStore.currentProjectId);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
watch(
() => showModalVisible.value,
(val) => {
if (val) {
initModules();
initGroupOptions();
}
}
);

View File

@ -10,32 +10,25 @@
@refresh="fetchData"
>
<template #left>
<!-- TODO 这个版本不上 -->
<!-- <a-radio-group v-model="showType" type="button" class="file-show-type mr-2">
<a-radio :value="testPlanTypeEnum.ALL" class="show-type-icon p-[2px]">{{
t('testPlan.testPlanIndex.all')
}}</a-radio>
<a-radio :value="testPlanTypeEnum.TEST_PLAN" class="show-type-icon p-[2px]">{{
t('testPlan.testPlanIndex.testPlan')
}}</a-radio>
<a-radio value="testPlanGroup" class="show-type-icon p-[2px]">{{
t('testPlan.testPlanIndex.testPlanGroup')
}}</a-radio>
</a-radio-group> -->
<a-popover title="" position="bottom">
<div class="flex">
<div class="one-line-text mr-1 max-h-[32px] max-w-[300px] text-[var(--color-text-1)]">
{{ props.activeFolder === 'all' ? t('testPlan.testPlanIndex.allTestPlan') : props.nodeName }}
</div>
<span class="text-[var(--color-text-4)]"> ({{ props.modulesCount[props.activeFolder] || 0 }})</span>
<div class="flex w-full items-center justify-between">
<div>
<a-radio-group v-model="showType" type="button" class="file-show-type mr-2">
<a-radio :value="testPlanTypeEnum.ALL" class="show-type-icon p-[2px]">{{
t('testPlan.testPlanIndex.all')
}}</a-radio>
<a-radio :value="testPlanTypeEnum.TEST_PLAN" class="show-type-icon p-[2px]">{{
t('testPlan.testPlanIndex.plan')
}}</a-radio>
<a-radio :value="testPlanTypeEnum.GROUP" class="show-type-icon p-[2px]">{{
t('testPlan.testPlanIndex.testPlanGroup')
}}</a-radio>
</a-radio-group></div
>
<div class="mr-[24px]">
<a-switch v-model="isArchived" size="small" type="line" @change="archivedChangeHandler" />
<span class="ml-1 text-[var(--color-text-3)]">{{ t('testPlan.testPlanGroup.seeArchived') }}</span>
</div>
<template #content>
<div class="max-w-[400px] text-[14px] font-medium text-[var(--color-text-1)]">
{{ props.nodeName }}
<span class="text-[var(--color-text-4)]">({{ props.modulesCount[props.activeFolder] || 0 }})</span>
</div>
</template>
</a-popover>
</div>
</template>
</MsAdvanceFilter>
<MsBaseTable
@ -43,60 +36,101 @@
ref="tableRef"
class="mt-4"
:action-config="testPlanBatchActions"
:selectable="hasOperationPermission && showType !== testPlanTypeEnum.ALL"
filter-icon-align-left
:selectable="hasOperationPermission"
:expanded-keys="expandedKeys"
v-on="propsEvent"
@batch-action="handleTableBatch"
@filter-change="filterChange"
@drag-change="handleDragChange"
>
<!-- :expanded-keys="expandedKeys" -->
<!-- TODO: 快捷创建暂时不上 -->
<!-- <template v-if="hasAnyPermission(['PROJECT_TEST_PLAN:READ+ADD'])" #quickCreate>
<a-form
v-if="showQuickCreateForm"
ref="quickCreateFormRef"
:model="quickCreateForm"
layout="inline"
size="small"
class="flex items-center"
>
<a-form-item
field="name"
:rules="[{ required: true, message: t('project.projectVersion.versionNameRequired') }]"
no-style
>
<a-input
v-model:model-value="quickCreateForm.name"
:max-length="255"
:placeholder="t('testPlan.testPlanGroup.newPlanPlaceHolder')"
class="w-[262px]"
/>
</a-form-item>
<a-form-item no-style>
<a-button type="outline" size="mini" class="ml-[12px] mr-[8px] px-[8px]" @click="quickCreateConfirm">
{{ t('common.confirm') }}
</a-button>
<a-button type="outline" class="arco-btn-outline--secondary px-[8px]" size="mini" @click="quickCreateCancel">
{{ t('common.cancel') }}
</a-button>
</a-form-item>
</a-form>
<MsButton v-if="!showQuickCreateForm && showType !== testPlanTypeEnum.ALL" @click="showQuickCreateForm = true">
<MsIcon type="icon-icon_add_outlined" size="14" class="mr-[8px]" />
{{ t('common.newCreate') }}
</MsButton>
<a-dropdown position="br" @select="handleSelect">
<MsButton v-if="!showQuickCreateForm && showType === testPlanTypeEnum.ALL">
<MsIcon type="icon-icon_add_outlined" size="14" class="mr-[8px]" />
{{ t('common.newCreate') }}
</MsButton>
<template #content>
<a-doption :value="testPlanTypeEnum.TEST_PLAN">{{ t('testPlan.testPlanIndex.createTestPlan') }}</a-doption>
<a-doption :value="testPlanTypeEnum.GROUP">{{ t('testPlan.testPlanIndex.createTestPlanGroup') }}</a-doption>
</template>
</a-dropdown>
</template> -->
<template #num="{ record }">
<!-- TODO 这个版本不做 -->
<!-- <div class="flex items-center">
<div v-if="record.childrenCount" class="mr-2 flex items-center" @click="expandHandler(record)">
<div class="flex items-center">
<div
v-if="record.type === testPlanTypeEnum.GROUP"
class="mr-2 flex items-center"
@click="expandHandler(record)"
>
<MsIcon
type="icon-icon_split-turn-down-left"
class="arrowIcon mr-1 text-[16px]"
type="icon-icon_split_turn-down_arrow"
class="arrowIcon mr-1 cursor-pointer text-[16px]"
:class="getIconClass(record)"
/>
<span :class="getIconClass(record)">{{ record.childrenCount }}</span>
<span :class="getIconClass(record)">{{ record.childrenCount || 0 }}</span>
</div>
<div
:class="[record.childrenCount ? 'pl-0' : 'pl-[36px]']"
class="one-line-text text-[rgb(var(--primary-5))]"
:class="`${
record.type === testPlanTypeEnum.TEST_PLAN ? 'text-[rgb(var(--primary-5))]' : ''
} one-line-text ${hasIndent(record)}`"
@click="openDetail(record.id, record.type)"
>{{ record.num }}</div
>
<a-tooltip position="right" :disabled="!record.schedule" :mouse-enter-delay="300">
<MsTag v-if="record.schedule" size="small" type="link" theme="outline" class="ml-2">{{
t('testPlan.testPlanIndex.timing')
}}</MsTag>
<!-- TODO 待联调定时任务 -->
<a-tooltip position="right" :disabled="record.schedule" :mouse-enter-delay="300">
<MsTag
v-if="record.schedule"
size="small"
:type="record.schedule ? 'link' : 'default'"
theme="outline"
class="ml-2"
>{{ t('testPlan.testPlanIndex.timing') }}</MsTag
>
<template #content>
<div>
<div v-if="record.schedule">
<div>{{ t('testPlan.testPlanIndex.scheduledTaskOpened') }}</div>
<div>{{ t('testPlan.testPlanIndex.nextExecutionTime') }}</div>
<div>---</div>
<div> {{ dayjs(record.createTime).format('YYYY-MM-DD HH:mm:ss') }}</div>
</div>
<div> {{ t('testPlan.testPlanIndex.scheduledTaskUnEnable') }} </div>
<div v-else> {{ t('testPlan.testPlanIndex.scheduledTaskUnEnable') }} </div>
</template>
</a-tooltip>
</div> -->
<div class="flex items-center">
<div class="one-line-text cursor-pointer text-[rgb(var(--primary-5))]" @click="openDetail(record.id)">{{
record.num
}}</div>
<a-tooltip position="right" :disabled="!record.schedule" :mouse-enter-delay="300">
<MsTag v-if="record.schedule" size="small" type="link" theme="outline" class="ml-2">{{
t('testPlan.testPlanIndex.timing')
}}</MsTag>
<template #content>
<div>
<div>{{ t('testPlan.testPlanIndex.scheduledTaskOpened') }}</div>
<div>{{ t('testPlan.testPlanIndex.nextExecutionTime') }}</div>
</div>
<div> {{ t('testPlan.testPlanIndex.scheduledTaskUnEnable') }} </div>
</template>
</a-tooltip></div
>
</div>
</template>
<template #[FilterSlotNameEnum.TEST_PLAN_STATUS_FILTER]="{ filterContent }">
<MsStatusTag :status="filterContent.value" />
@ -208,7 +242,7 @@
<MsButton
v-if="hasAnyPermission(['PROJECT_TEST_PLAN:READ+UPDATE']) && record.status !== 'ARCHIVED'"
class="!mx-0"
@click="emit('editOrCopy', record.id, false)"
@click="emit('edit', record)"
>{{ t('common.edit') }}</MsButton
>
<a-divider
@ -224,7 +258,7 @@
record.status !== 'ARCHIVED'
"
class="!mx-0"
@click="emit('editOrCopy', record.id, true)"
@click="copyTestPlanOrGroup(record.id)"
>{{ t('common.copy') }}</MsButton
>
<a-divider
@ -236,10 +270,7 @@
direction="vertical"
:margin="8"
></a-divider>
<MsTableMoreAction
:list="getMoreActions(record.status, record.functionalCaseCount)"
@select="handleMoreActionSelect($event, record)"
/>
<MsTableMoreAction :list="getMoreActions(record)" @select="handleMoreActionSelect($event, record)" />
</div>
</template>
</MsBaseTable>
@ -255,7 +286,7 @@
<template #title>
{{ t('testPlan.testPlanIndex.batchExecution') }}
</template>
<a-radio-group>
<a-radio-group v-model="executeType">
<a-radio value="serial">{{ t('testPlan.testPlanIndex.serial') }}</a-radio>
<a-radio value="parallel">{{ t('testPlan.testPlanIndex.parallel') }}</a-radio>
</a-radio-group>
@ -277,9 +308,11 @@
:current-select-count="batchParams.currentSelectCount || 0"
:get-module-tree-api="getTestPlanModule"
:ok-loading="okLoading"
:type="showType"
@save="handleMoveOrCopy"
/>
<ScheduledModal v-model:visible="showScheduledTaskModal" />
<!-- TODO 待联调定时任务 -->
<ScheduledModal v-model:visible="showScheduledTaskModal" :type="currentPlanType" @close="resetPlanType" />
<ActionModal v-model:visible="showStatusDeleteModal" :record="activeRecord" @success="fetchData()" />
<BatchEditModal
v-model:visible="showEditModel"
@ -295,7 +328,7 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { Message } from '@arco-design/web-vue';
import { FormInstance, Message } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import dayjs from 'dayjs';
@ -303,7 +336,12 @@
import { FilterFormItem } from '@/components/pure/ms-advance-filter/type';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { BatchActionParams, BatchActionQueryParams, MsTableColumn } from '@/components/pure/ms-table/type';
import type {
BatchActionParams,
BatchActionQueryParams,
MsTableColumn,
MsTableProps,
} from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
@ -316,15 +354,19 @@
import StatusProgress from './statusProgress.vue';
import {
addTestPlan,
archivedPlan,
batchArchivedPlan,
batchCopyPlan,
batchDeletePlan,
batchMovePlan,
deletePlan,
dragPlanOnGroup,
getPlanPassRate,
getTestPlanDetail,
getTestPlanList,
getTestPlanModule,
testPlanAndGroupCopy,
updateTestPlan,
} from '@/api/modules/test-plan/testPlan';
import { useI18n } from '@/hooks/useI18n';
@ -333,8 +375,14 @@
import { characterLimit } from '@/utils';
import { hasAnyPermission } from '@/utils/permission';
import { ModuleTreeNode } from '@/models/common';
import type { PassRateCountDetail, planStatusType, TestPlanItem } from '@/models/testPlan/testPlan';
import { DragSortParams, ModuleTreeNode } from '@/models/common';
import type {
AddTestPlanParams,
BatchMoveParams,
moduleForm,
PassRateCountDetail,
TestPlanItem,
} from '@/models/testPlan/testPlan';
import { TestPlanRouteEnum } from '@/enums/routeEnum';
import { ColumnEditTypeEnum, TableKeyEnum } from '@/enums/tableEnum';
import { FilterSlotNameEnum } from '@/enums/tableFilterEnum';
@ -361,9 +409,13 @@
const emit = defineEmits<{
(e: 'init', params: any): void;
(e: 'editOrCopy', id: string, isCopy: boolean): void;
(e: 'edit', record: TestPlanItem): void;
}>();
const isArchived = ref<boolean>(false);
const keyword = ref<string>('');
const currentPlanType = ref<keyof typeof testPlanTypeEnum>(testPlanTypeEnum.TEST_PLAN);
const hasOperationPermission = computed(() =>
hasAnyPermission(['PROJECT_TEST_PLAN:READ+UPDATE', 'PROJECT_TEST_PLAN:READ+EXECUTE', 'PROJECT_TEST_PLAN:READ+ADD'])
);
@ -496,7 +548,7 @@
];
/**
* 更新测试计划名称
* 更新测试计划以及测试计划组
*/
async function updatePlanName(record: TestPlanItem) {
try {
@ -511,69 +563,92 @@
return Promise.resolve(true);
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
return Promise.resolve(false);
}
}
const keyword = ref<string>('');
const showType = ref<keyof typeof testPlanTypeEnum>(testPlanTypeEnum.TEST_PLAN);
const showType = ref<keyof typeof testPlanTypeEnum>(testPlanTypeEnum.ALL);
const testPlanBatchActions = {
baseAction: [
// TODO
// {
// label: 'testPlan.testPlanIndex.execute',
// eventTag: 'execute',
// permission: ['PROJECT_TEST_PLAN:READ+EXECUTE'],
// },
{
label: 'common.edit',
eventTag: 'edit',
permission: ['PROJECT_TEST_PLAN:READ+UPDATE'],
},
{
label: 'common.copy',
eventTag: 'copy',
permission: ['PROJECT_TEST_PLAN:READ+ADD'],
},
// {
// label: 'common.export',
// eventTag: 'export',
// },
],
moreAction: [
// {
// label: 'testPlan.testPlanIndex.openTimingTask',
// eventTag: 'openTimingTask',
// permission: ['PROJECT_TEST_PLAN:READ+UPDATE'],
// },
// {
// label: 'testPlan.testPlanIndex.closeTimingTask',
// eventTag: 'closeTimingTask',
// permission: ['PROJECT_TEST_PLAN:READ+UPDATE'],
// },
{
label: 'common.move',
eventTag: 'move',
permission: ['PROJECT_TEST_PLAN:READ+UPDATE'],
},
{
label: 'common.archive',
eventTag: 'archive',
permission: ['PROJECT_TEST_PLAN:READ+UPDATE'],
},
{
isDivider: true,
},
{
label: 'common.delete',
eventTag: 'delete',
danger: true,
permission: ['PROJECT_TEST_PLAN:READ+DELETE'],
},
],
};
//
function hasIndent(record: TestPlanItem) {
return (showType.value === 'ALL' || showType.value === 'GROUP') &&
record.type === testPlanTypeEnum.TEST_PLAN &&
record.groupId &&
record.groupId !== 'NONE'
? 'pl-[36px]'
: '';
}
const batchCopyActions = [
{
label: 'common.copy',
eventTag: 'copy',
permission: ['PROJECT_TEST_PLAN:READ+ADD'],
},
];
const baseActions = [
{
label: 'testPlan.testPlanIndex.execute',
eventTag: 'execute',
permission: ['PROJECT_TEST_PLAN:READ+EXECUTE'],
},
{
label: 'common.edit',
eventTag: 'edit',
permission: ['PROJECT_TEST_PLAN:READ+UPDATE'],
},
// TODO
// {
// label: 'common.export',
// eventTag: 'export',
// },
];
const moreAction = [
{
label: 'testPlan.testPlanIndex.openTimingTask',
eventTag: 'openTimingTask',
permission: ['PROJECT_TEST_PLAN:READ+UPDATE'],
},
{
label: 'testPlan.testPlanIndex.closeTimingTask',
eventTag: 'closeTimingTask',
permission: ['PROJECT_TEST_PLAN:READ+UPDATE'],
},
{
label: 'common.move',
eventTag: 'move',
permission: ['PROJECT_TEST_PLAN:READ+UPDATE'],
},
{
label: 'common.archive',
eventTag: 'archive',
permission: ['PROJECT_TEST_PLAN:READ+UPDATE'],
},
{
isDivider: true,
},
{
label: 'common.delete',
eventTag: 'delete',
danger: true,
permission: ['PROJECT_TEST_PLAN:READ+DELETE'],
},
];
const testPlanBatchActions = computed(() => {
if (showType.value === testPlanTypeEnum.GROUP) {
return {
baseAction: baseActions,
moreAction,
};
}
return {
baseAction: [...baseActions, ...batchCopyActions],
moreAction,
};
});
const archiveActions: ActionsItem[] = [
{
@ -590,14 +665,47 @@
},
];
function getMoreActions(status: planStatusType, useCount: number) {
const createScheduledActions: ActionsItem[] = [
{
label: 'testPlan.testPlanIndex.createScheduledTask',
eventTag: 'createScheduledTask',
permission: ['PROJECT_TEST_PLAN:READ+UPDATE'],
},
];
const updateAndDeleteScheduledActions: ActionsItem[] = [
{
label: 'testPlan.testPlanIndex.updateScheduledTask',
eventTag: 'updateScheduledTask',
permission: ['PROJECT_TEST_PLAN:READ+UPDATE'],
},
{
label: 'testPlan.testPlanIndex.deleteScheduledTask',
eventTag: 'deleteScheduledTask',
permission: ['PROJECT_TEST_PLAN:READ+UPDATE'],
},
];
function getMoreActions(record: TestPlanItem) {
const { status: planStatus, functionalCaseCount: useCount, schedule } = record;
//
const copyAction =
useCount > 0 && hasAnyPermission(['PROJECT_TEST_PLAN:READ+ADD']) && status !== 'ARCHIVED' ? copyActions : [];
//
if (status === 'ARCHIVED' || status === 'PREPARED' || status === 'UNDERWAY') {
useCount > 0 && hasAnyPermission(['PROJECT_TEST_PLAN:READ+ADD']) && planStatus !== 'ARCHIVED' ? copyActions : [];
// TODO
let scheduledTaskAction: ActionsItem[] = [];
if (planStatus !== 'ARCHIVED') {
scheduledTaskAction = schedule ? updateAndDeleteScheduledActions : createScheduledActions;
}
// &
const archiveAction =
(record.type === testPlanTypeEnum.GROUP && record.childrenCount < 1) ||
(record.type === testPlanTypeEnum.TEST_PLAN && record.groupId && record.groupId !== 'NONE')
? []
: archiveActions;
//
if (planStatus === 'ARCHIVED' || planStatus === 'PREPARED' || planStatus === 'UNDERWAY') {
return [
...copyAction,
...scheduledTaskAction,
{
label: 'common.delete',
danger: true,
@ -608,7 +716,8 @@
}
return [
...copyAction,
...archiveActions,
...archiveAction,
...scheduledTaskAction,
{
isDivider: true,
},
@ -621,16 +730,20 @@
];
}
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(
const tableProps = ref<Partial<MsTableProps<TestPlanItem>>>({
tableKey: TableKeyEnum.TEST_PLAN_ALL_TABLE,
selectable: true,
showSetting: true,
heightUsed: 236,
paginationSize: 'mini',
showSelectorAll: true,
draggable: { type: 'handle' },
draggableCondition: true,
});
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector, resetFilterParams } = useTable(
getTestPlanList,
{
tableKey: TableKeyEnum.TEST_PLAN_ALL_TABLE,
selectable: true,
showSetting: true,
heightUsed: 236,
paginationSize: 'mini',
showSelectorAll: false,
},
tableProps.value,
(item) => {
return {
...item,
@ -661,6 +774,13 @@
if (isSetDefaultKey) {
moduleIds = [];
}
const filterParams = {
...propsRes.value.filter,
};
if (isArchived.value) {
filterParams.status = ['ARCHIVED'];
}
return {
type: showType.value,
moduleIds,
@ -670,9 +790,10 @@
selectIds: batchParams.value.selectedIds || [],
keyword: keyword.value,
condition: {
filter: propsRes.value.filter,
filter: filterParams,
keyword: keyword.value,
},
filter: filterParams,
combine: {
...batchParams.value.condition,
},
@ -704,6 +825,7 @@
defaultCountDetailMap.value[item.id] = item;
});
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
@ -715,7 +837,10 @@
}
//
function openDetail(id: string) {
function openDetail(id: string, type?: keyof typeof testPlanTypeEnum) {
if (type && type === testPlanTypeEnum.GROUP) {
return;
}
router.push({
name: TestPlanRouteEnum.TEST_PLAN_INDEX_DETAIL,
query: {
@ -727,7 +852,9 @@
/**
* 批量执行
*/
const executeType = ref('serial');
const executeVisible = ref<boolean>(false);
function handleExecute() {
executeVisible.value = true;
}
@ -737,11 +864,16 @@
}
const confirmLoading = ref<boolean>(false);
/**
* 执行
* 执行 TODO 待联调
*/
function executeHandler() {}
function executeHandler() {
try {
Message.success(t('case.detail.execute.success'));
} catch (error) {
console.log(error);
}
}
/**
* 批量复制或者移动
@ -759,11 +891,13 @@
/**
* 批量移动或复制保存
*/
async function handleMoveOrCopy() {
async function handleMoveOrCopy(moveForm: moduleForm) {
okLoading.value = true;
try {
const params = {
const params: BatchMoveParams = {
excludeIds: batchParams.value?.excludeIds || [],
selectIds: batchParams.value.selectedIds || [],
selectAll: !!batchParams.value?.selectAll,
condition: {
keyword: keyword.value,
filter: {},
@ -773,6 +907,8 @@
moduleIds: [...selectNodeKeys.value],
type: showType.value,
moduleId: selectNodeKeys.value[0],
targetId: moveForm.moveType === 'MODULE' ? selectNodeKeys.value[0] : moveForm.targetId,
moveType: moveForm.moveType,
};
if (modeType.value === 'copy') {
await batchCopyPlan(params);
@ -791,19 +927,24 @@
}
/**
* 打开关闭定时任务
* 打开关闭定时任务 TODO 待联调
*/
function handleStatusTimingTask(status: boolean) {}
function handleStatusTimingTask(enable: boolean) {}
/**
* 归档
* 归档测试计划以及计划组
*/
function handleArchive() {
openModal({
type: 'warning',
title: t('testPlan.testPlanIndex.confirmBatchArchivePlan', {
count: batchParams.value.currentSelectCount,
}),
title: t(
showType.value === testPlanTypeEnum.TEST_PLAN
? 'testPlan.testPlanIndex.confirmBatchArchivePlan'
: 'testPlan.testPlanGroup.batchArchivedGroup',
{
count: batchParams.value.currentSelectCount,
}
),
content: t('testPlan.testPlanIndex.confirmBatchArchivePlanContent'),
okText: t('common.archive'),
cancelText: t('common.cancel'),
@ -813,7 +954,9 @@
onBeforeOk: async () => {
try {
await batchArchivedPlan({
excludeIds: batchParams.value?.excludeIds || [],
selectIds: batchParams.value.selectedIds || [],
selectAll: !!batchParams.value?.selectAll,
condition: {
keyword: keyword.value,
filter: propsRes.value.filter,
@ -834,14 +977,19 @@
});
}
/**
* 删除
* 删除测试计划以及计划组
*/
function handleDelete() {
openModal({
type: 'error',
title: t('testPlan.testPlanIndex.confirmBatchDeletePlan', {
count: batchParams.value.currentSelectCount,
}),
title: t(
showType.value === testPlanTypeEnum.GROUP
? 'testPlan.testPlanGroup.confirmBatchDeletePlanGroup'
: 'testPlan.testPlanIndex.confirmBatchDeletePlan',
{
count: batchParams.value.currentSelectCount,
}
),
content: t('testPlan.testPlanIndex.confirmBatchDeletePlanContent'),
okText: t('common.confirmDelete'),
cancelText: t('common.cancel'),
@ -923,27 +1071,65 @@
}
}
function copyHandler(record: TestPlanItem) {
emit('editOrCopy', record.id as string, true);
const showScheduledTaskModal = ref<boolean>(false);
function handleScheduledTask(record: TestPlanItem) {
currentPlanType.value = record.type;
showScheduledTaskModal.value = true;
}
const showScheduledTaskModal = ref<boolean>(false);
function handleScheduledTask() {
showScheduledTaskModal.value = true;
function resetPlanType() {
currentPlanType.value = testPlanTypeEnum.TEST_PLAN;
}
const showStatusDeleteModal = ref<boolean>(false);
const activeRecord = ref<TestPlanItem>();
// :
async function handleDeleteGroup(record: TestPlanItem) {
try {
await deletePlan(record.id);
fetchData();
Message.success(t('common.deleteSuccess'));
} catch (error) {
console.log(error);
}
}
function deleteStatusHandler(record: TestPlanItem) {
if (record.type === testPlanTypeEnum.GROUP && !record.childrenCount) {
handleDeleteGroup(record);
return;
}
activeRecord.value = cloneDeep(record);
showStatusDeleteModal.value = true;
}
// TODO
async function handleDragChange(params: DragSortParams) {
try {
await dragPlanOnGroup(params);
Message.success(t('caseManagement.featureCase.sortSuccess'));
fetchData();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
//
function archiveHandle(record: TestPlanItem) {
let archiveTitle = t('common.archiveConfirmTitle', { name: characterLimit(record.name) });
let archiveContent = t('testPlan.testPlanIndex.confirmArchivePlan');
if (record.type === 'GROUP') {
archiveTitle = t('testPlan.testPlanGroup.planGroupArchiveTitle', {
name: characterLimit(record.name),
});
archiveContent = t('testPlan.testPlanGroup.planGroupArchiveContent');
}
openModal({
type: 'warning',
title: t('common.archiveConfirmTitle', { name: characterLimit(record.name) }),
content: t('testPlan.testPlanIndex.confirmArchivePlan'),
title: archiveTitle,
content: archiveContent,
okText: t('common.archive'),
cancelText: t('common.cancel'),
okButtonProps: {
@ -962,13 +1148,23 @@
});
}
async function copyTestPlanOrGroup(id: string) {
try {
await testPlanAndGroupCopy(id);
Message.success(t('common.copySuccess'));
fetchData();
} catch (error) {
console.log(error);
}
}
function handleMoreActionSelect(item: ActionsItem, record: TestPlanItem) {
switch (item.eventTag) {
case 'copy':
copyHandler(record);
copyTestPlanOrGroup(record.id as string);
break;
case 'createScheduledTask':
handleScheduledTask();
handleScheduledTask(record);
break;
case 'delete':
deleteStatusHandler(record);
@ -982,19 +1178,21 @@
}
const expandedKeys = ref<string[]>([]);
// TODO
// function expandHandler(record: any) {
// if (expandedKeys.value.includes(record.id)) {
// expandedKeys.value = expandedKeys.value.filter((key) => key !== record.id);
// } else {
// expandedKeys.value = [...expandedKeys.value, record.id];
// }
// }
// TODO
// function getIconClass(record: any) {
// return expandedKeys.value.includes(record.id) ? 'text-[rgb(var(--primary-5))]' : 'text-[var(--color-text-4)]';
// }
//
function expandHandler(record: TestPlanItem) {
if (expandedKeys.value.includes(record.id)) {
expandedKeys.value = expandedKeys.value.filter((key) => key !== record.id);
} else {
expandedKeys.value = [...expandedKeys.value, record.id];
if (record.type === 'GROUP' && record.childrenCount) {
const testPlanId = record.children.map((item: TestPlanItem) => item.id);
getStatistics(testPlanId);
}
}
}
function getIconClass(record: TestPlanItem) {
return expandedKeys.value.includes(record.id) ? 'text-[rgb(var(--primary-5))]' : 'text-[var(--color-text-4)]';
}
/** *
* 高级检索
@ -1007,6 +1205,9 @@
() => showType.value,
(val) => {
if (val) {
tableProps.value.draggableCondition = hasAnyPermission(['PROJECT_TEST_PLAN:READ+UPDATE']) && val === 'ALL';
expandedKeys.value = [];
resetFilterParams();
fetchData();
}
}
@ -1051,6 +1252,80 @@
emitTableParams();
}
const showQuickCreateForm = ref(false);
const quickCreateFormRef = ref<FormInstance>();
const initPlanGroupForm: AddTestPlanParams = {
groupId: 'NONE',
name: '',
projectId: appStore.currentProjectId,
moduleId: '',
cycle: [],
tags: [],
description: '',
testPlanning: false,
automaticStatusUpdate: true,
repeatCase: false,
passThreshold: 100,
type: testPlanTypeEnum.GROUP,
baseAssociateCaseRequest: { selectIds: [], selectAll: false, condition: {} },
};
const quickCreateForm = ref<AddTestPlanParams>(cloneDeep(initPlanGroupForm));
const quickCreateLoading = ref(false);
function quickCreateCancel() {
showQuickCreateForm.value = false;
quickCreateForm.value = cloneDeep(initPlanGroupForm);
quickCreateFormRef.value?.resetFields();
}
/**
* 快速创建测试计划或者测试计划组
*/
const createType = ref<keyof typeof testPlanTypeEnum>(showType.value);
// TODO:
function quickCreateConfirm() {
quickCreateFormRef.value?.validate(async (errors) => {
if (!errors) {
try {
quickCreateLoading.value = true;
const params = {
...cloneDeep(quickCreateForm.value),
groupId: 'NONE',
projectId: appStore.currentProjectId,
moduleId: props.activeFolder === 'all' ? 'root' : props.activeFolder,
testPlanning: false,
automaticStatusUpdate: true,
repeatCase: false,
passThreshold: 100,
type: showType.value === testPlanTypeEnum.ALL ? createType.value : showType.value,
};
await addTestPlan(params);
Message.success(t('common.createSuccess'));
quickCreateCancel();
fetchData();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
quickCreateLoading.value = false;
}
}
});
}
// TODO:
function handleSelect(value: string | number | Record<string, any> | undefined) {
showQuickCreateForm.value = true;
createType.value = value as keyof typeof testPlanTypeEnum;
}
//
function archivedChangeHandler() {
resetFilterParams();
fetchData();
}
defineExpose({
fetchData,
emitTableParams,
@ -1060,16 +1335,12 @@
</script>
<style scoped lang="less">
// TODO
// :deep(.arco-table-cell-expand-icon .arco-table-cell-inline-icon) {
// display: none;
// }
// :deep(.arco-table-cell-align-left) > span:first-child {
// padding-left: 0 !important;
// }
// .arrowIcon {
// transform: scaleX(-1);
// }
:deep(.arco-table-cell-expand-icon .arco-table-cell-inline-icon) {
display: none;
}
:deep(.arco-table-cell-align-left) > span:first-child {
padding-left: 0 !important;
}
.popover-label-td {
@apply flex items-center;

View File

@ -17,16 +17,18 @@
{{ item.label }}
</span>
</a-option>
<template #footer>
<!-- TODO :暂时不做 -->
<!-- <template #footer>
<div class="mb-[6px] mt-[4px] p-[3px_8px]">
<MsButton type="text" class="text-[rgb(var(--primary-5))]" @click="createCustomFrequency">
{{ t('testPlan.testPlanIndex.customFrequency') }}
</MsButton>
</div>
</template>
</template> -->
</a-select>
</a-form-item>
<a-radio-group v-model="form.env" class="mb-4">
<!-- TOTO 环境暂时不上 -->
<!-- <a-radio-group v-model="form.env" class="mb-4">
<a-radio value="">
{{ t('testPlan.testPlanIndex.defaultEnv') }}
<span class="float-right mx-1 mt-[1px]">
@ -36,12 +38,13 @@
</span>
</a-radio>
<a-radio value="new"> {{ t('testPlan.testPlanIndex.newEnv') }}</a-radio>
</a-radio-group>
<a-radio-group v-model="form.methods">
</a-radio-group> -->
<a-radio-group v-if="props.type === testPlanTypeEnum.GROUP" v-model="form.methods">
<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 :label="t('testPlan.testPlanIndex.resourcePool')" asterisk-position="end" class="mb-0">
<!-- TODO 资源池暂时不做 -->
<!-- <a-form-item :label="t('testPlan.testPlanIndex.resourcePool')" asterisk-position="end" class="mb-0">
<a-select
v-model="form.resourcePoolIds"
:placeholder="t('common.pleaseSelect')"
@ -65,7 +68,7 @@
</div>
</a-option>
</a-select>
</a-form-item>
</a-form-item> -->
</a-form>
<template #footer>
<div class="flex items-center justify-between">
@ -104,16 +107,19 @@
import { useAppStore } from '@/store';
import type { ResourcesItem } from '@/models/testPlan/testPlan';
import { testPlanTypeEnum } from '@/enums/testPlanEnum';
const appStore = useAppStore();
const { t } = useI18n();
const props = defineProps<{
visible: boolean;
type: keyof typeof testPlanTypeEnum;
}>();
const emit = defineEmits<{
(e: 'update:visible', val: boolean): void;
(e: 'close'): void;
}>();
const showModalVisible = useVModel(props, 'visible', emit);
@ -140,6 +146,7 @@
showModalVisible.value = false;
formRef.value?.resetFields();
resetForm();
emit('close');
}
const syncFrequencyOptions = [

View File

@ -153,7 +153,7 @@
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
import AssociateDrawer from './components/associateDrawer.vue';
import { addTestPlan, copyTestPlan, getTestPlanDetail, updateTestPlan } from '@/api/modules/test-plan/testPlan';
import { addTestPlan, getTestPlanDetail, updateTestPlan } from '@/api/modules/test-plan/testPlan';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
@ -166,7 +166,6 @@
const props = defineProps<{
planId?: string;
moduleTree?: ModuleTreeNode[];
isCopy: boolean;
moduleId?: string;
}>();
const innerVisible = defineModel<boolean>('visible', {
@ -288,18 +287,6 @@
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',
@ -311,31 +298,8 @@
await addTestPlan(params);
Message.success(t('common.createSuccess'));
} else {
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'));
await updateTestPlan(params);
Message.success(t('common.updateSuccess'));
}
emit('loadPlanList');
} catch (error) {
@ -357,11 +321,6 @@
if (props.planId?.length) {
const result = await getTestPlanDetail(props.planId);
form.value = cloneDeep(result);
if (props.isCopy) {
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];
form.value.passThreshold = parseFloat(result.passThreshold.toString());
@ -386,17 +345,11 @@
);
const modelTitle = computed(() => {
if (props.planId) {
return props.isCopy ? t('testPlan.testPlanIndex.copyTestPlan') : t('testPlan.testPlanIndex.updateTestPlan');
}
return t('testPlan.testPlanIndex.createTestPlan');
return props.planId ? t('testPlan.testPlanIndex.updateTestPlan') : t('testPlan.testPlanIndex.createTestPlan');
});
const okText = computed(() => {
if (props.planId) {
return props.isCopy ? t('common.copy') : t('common.update');
}
return t('common.create');
return props.planId ? t('common.update') : t('common.create');
});
const getSelectedCount = computed(() => {

View File

@ -124,6 +124,7 @@
/>
<ExecuteHistory v-if="activeTab === 'executeHistory'" />
</MsCard>
<!-- TODO 待联调关联用例 目前可以暂时关联功能用例 -->
<AssociateDrawer
v-model:visible="caseAssociateVisible"
:associated-ids="detail.repeatCase ? hasSelectedIds : []"
@ -131,10 +132,10 @@
:test-plan-id="planId"
@success="handleSuccess"
/>
<CreateAndEditPlanDrawer
v-model:visible="showPlanDrawer"
:plan-id="planId"
:is-copy="isCopy"
:module-tree="testPlanTree"
@load-plan-list="successHandler"
/>

View File

@ -9,14 +9,22 @@
:placeholder="t('caseManagement.featureCase.searchTip')"
allow-clear
/>
<a-button
<a-dropdown-button
v-permission="['PROJECT_TEST_PLAN:READ+ADD']"
class="ml-2"
type="primary"
@click="handleSelect('createPlan')"
>
{{ t('common.newCreate') }}
</a-button>
<template #icon>
<icon-down />
</template>
<template #content>
<a-doption value="createGroup" @click="handleSelect('createGroup')">
{{ t('testPlan.testPlanIndex.testPlanGroup') }}
</a-doption>
</template>
</a-dropdown-button>
</div>
<div class="test-plan h-[100%]">
@ -85,7 +93,7 @@
:module-tree="folderTree"
:node-name="nodeName"
@init="initModulesCount"
@edit-or-copy="handleEditOrCopy"
@edit="handleEdit"
/>
</div>
</template>
@ -95,7 +103,14 @@
:plan-id="planId"
:module-id="selectedKeys[0]"
:module-tree="folderTree"
:is-copy="isCopy"
@close="resetPlanId"
@load-plan-list="loadPlanList"
/>
<CreateAndUpdatePlanGroup
v-model:visible="showPlanGroupModel"
:plan-group-id="planId"
:module-tree="folderTree"
:module-id="selectedKeys[0]"
@close="resetPlanId"
@load-plan-list="loadPlanList"
/>
@ -114,14 +129,16 @@
import PlanTable from './components/planTable.vue';
import TestPlanTree from './components/testPlanTree.vue';
import CreateAndEditPlanDrawer from './createAndEditPlanDrawer.vue';
import CreateAndUpdatePlanGroup from '@/views/test-plan/testPlan/planGroup/createAndUpdatePlanGroup.vue';
import { createPlanModuleTree, getPlanModulesCount } from '@/api/modules/test-plan/testPlan';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { hasAnyPermission } from '@/utils/permission';
import type { CreateOrUpdateModule } from '@/models/caseManagement/featureCase';
import { ModuleTreeNode, TableQueryParams } from '@/models/common';
import type { TestPlanItem } from '@/models/testPlan/testPlan';
import { testPlanTypeEnum } from '@/enums/testPlanEnum';
import Message from '@arco-design/web-vue/es/message';
@ -221,27 +238,36 @@
}
}
const showPlanDrawer = ref(false);
const showPlanDrawer = ref<boolean>(false);
const showPlanGroupModel = ref<boolean>(false);
function handleSelect(value: string | number | Record<string, any> | undefined) {
switch (value) {
case 'createPlan':
showPlanDrawer.value = true;
break;
case 'createGroup':
showPlanGroupModel.value = true;
break;
default:
break;
}
}
const planId = ref('');
const isCopy = ref<boolean>(false);
function handleEditOrCopy(id: string, isCopyFlag: boolean) {
planId.value = id;
isCopy.value = isCopyFlag;
showPlanDrawer.value = true;
const planId = ref<string>();
function handleEdit(record: TestPlanItem) {
planId.value = record.id;
if (record.type === testPlanTypeEnum.TEST_PLAN) {
showPlanDrawer.value = true;
} else {
showPlanGroupModel.value = true;
}
}
function resetPlanId() {
planId.value = '';
}
function loadPlanList() {
planTableRef.value?.fetchData();
}

View File

@ -1,5 +1,6 @@
export default {
'testPlan.testPlanIndex.createTestPlan': 'create test plan',
'testPlan.testPlanIndex.createTestPlanGroup': 'New Plan group',
'testPlan.testPlanIndex.updateTestPlan': 'update test plan',
'testPlan.testPlanIndex.copyTestPlan': 'copy test plan',
'testPlan.testPlanIndex.allTestPlan': 'All test Plans',
@ -11,7 +12,8 @@ export default {
'testPlan.testPlanIndex.rename': 'rename',
'testPlan.testPlanIndex.all': 'All',
'testPlan.testPlanIndex.testPlan': 'Test plan',
'testPlan.testPlanIndex.testPlanGroup': 'Test planning groups',
'testPlan.testPlanIndex.plan': 'Plan',
'testPlan.testPlanIndex.testPlanGroup': 'Planning groups',
'testPlan.testPlanIndex.testPlanName': 'name',
'testPlan.testPlanIndex.ID': 'ID',
'testPlan.testPlanIndex.executionResult': 'Execution Result',
@ -46,6 +48,7 @@ export default {
'testPlan.testPlanIndex.selectedCount': '{count} data selected',
'testPlan.testPlanIndex.createScheduledTask': 'Create Scheduled Task',
'testPlan.testPlanIndex.updateScheduledTask': 'Update Scheduled Task',
'testPlan.testPlanIndex.deleteScheduledTask': 'Delete Scheduled Task',
'testPlan.testPlanIndex.configuration': 'config',
'testPlan.testPlanIndex.triggerTime': 'Trigger time',
'testPlan.testPlanIndex.envTip': 'Use case save environment',
@ -109,4 +112,25 @@ export default {
'testPlan.featureCase.autoNextTip1': 'Enable: After submitting the results, jump to the next case',
'testPlan.featureCase.autoNextTip2': 'Close: After submitting the results, it is still in the current state',
'testPlan.executeHistory.executionStartAndEndTime': 'Execution start and end time',
'testPlan.testPlanGroup.seeArchived': 'Only see archived',
'testPlan.testPlanGroup.planNamePlaceholder': 'Please enter the name of the test plan group',
'testPlan.testPlanGroup.name': 'Group Name',
'testPlan.testPlanGroup.newPlanGroupTitle': 'New Project group',
'testPlan.testPlanGroup.updatePlanGroupTitle': 'Update Planning group {name}',
'testPlan.testPlanGroup.copyPlanGroupTitle': 'Copy planning groups {name}',
'testPlan.testPlanGroup.newPlanPlaceHolder': 'Please enter name',
'testPlan.testPlanGroup.noPlanOnGroupArchiveTitle': '{name} No archived test plan',
'testPlan.testPlanGroup.noPlanOnGroupArchiveContent': 'The test plan can be archived if the status is completed',
'testPlan.testPlanGroup.allPlanIsCompletedAndArchivedContent':
'Test plans in the Completed state can be archived. Archived plans are not affected',
'testPlan.testPlanGroup.unCompletedAndArchivedContent':
'Status as completed test plans can be archived, unfinished and archived plans are not affected',
'testPlan.testPlanGroup.planGroupArchiveTitle': 'Confirm archive {name} plan group and plan?',
'testPlan.testPlanGroup.planGroupArchiveContent':
'After archived, plan and carry out information no longer update and edit data unrecoverable, please careful operation.',
'testPlan.testPlanGroup.planGroupDeleteContent':
'Planning groups choose file has been completed and the case information, and the results will be retained, If you continue to delete, the data will not be recovered, please be careful!',
'testPlan.testPlanGroup.selectTestPlanGroupPlaceHolder': 'Please select the Plan group',
'testPlan.testPlanGroup.batchArchivedGroup': 'Confirm archive: {count} test plan groups',
'testPlan.testPlanGroup.confirmBatchDeletePlanGroup': 'Are you sure to delete {count} test plan groups?',
};

View File

@ -1,5 +1,6 @@
export default {
'testPlan.testPlanIndex.createTestPlan': '创建测试计划',
'testPlan.testPlanIndex.createTestPlan': '新建测试计划',
'testPlan.testPlanIndex.createTestPlanGroup': '新建计划组',
'testPlan.testPlanIndex.updateTestPlan': '更新测试计划',
'testPlan.testPlanIndex.copyTestPlan': '复制测试计划',
'testPlan.testPlanIndex.allTestPlan': '全部测试计划',
@ -11,7 +12,8 @@ export default {
'testPlan.testPlanIndex.rename': '重命名',
'testPlan.testPlanIndex.all': '全部',
'testPlan.testPlanIndex.testPlan': '测试计划',
'testPlan.testPlanIndex.testPlanGroup': '测试计划组',
'testPlan.testPlanIndex.plan': '计划',
'testPlan.testPlanIndex.testPlanGroup': '计划组',
'testPlan.testPlanIndex.testPlanName': '测试计划名称',
'testPlan.testPlanIndex.ID': 'ID',
'testPlan.testPlanIndex.executionResult': '执行结果',
@ -44,6 +46,7 @@ export default {
'testPlan.testPlanIndex.selectedCount': '(已选 {count} 项数据)',
'testPlan.testPlanIndex.createScheduledTask': '创建定时任务',
'testPlan.testPlanIndex.updateScheduledTask': '更新定时任务',
'testPlan.testPlanIndex.deleteScheduledTask': '删除定时任务',
'testPlan.testPlanIndex.configuration': '配置',
'testPlan.testPlanIndex.triggerTime': '触发时间',
'testPlan.testPlanIndex.envTip': '用例保存的环境',
@ -103,4 +106,20 @@ export default {
'testPlan.featureCase.autoNextTip1': '开启:提交结果后,跳转至下一条用例',
'testPlan.featureCase.autoNextTip2': '关闭:提交结果后,还在当前',
'testPlan.executeHistory.executionStartAndEndTime': '执行起止时间',
'testPlan.testPlanGroup.seeArchived': '只看已归档',
'testPlan.testPlanGroup.planNamePlaceholder': '请输入测试计划组名称',
'testPlan.testPlanGroup.name': '计划组名称',
'testPlan.testPlanGroup.newPlanGroupTitle': '新建计划组',
'testPlan.testPlanGroup.updatePlanGroupTitle': '更新计划组',
'testPlan.testPlanGroup.copyPlanGroupTitle': '复制计划组',
'testPlan.testPlanGroup.newPlanPlaceHolder': '请输入名称',
'testPlan.testPlanGroup.planGroupArchiveTitle': '确认归档 {name} 计划组以及计划吗?',
'testPlan.testPlanGroup.planGroupArchiveContent':
'归档后,计划执行信息不再更新且不可编辑,数据不可恢复,请谨慎操作!',
'testPlan.testPlanGroup.planGroupDeleteContent':
'计划组 已完成 选择归档,用例信息及执行结果都将被保留;若继续删除,数据将不会恢复,请谨慎操作!',
'testPlan.testPlanGroup.module': '模块',
'testPlan.testPlanGroup.selectTestPlanGroupPlaceHolder': '请选择计划组',
'testPlan.testPlanGroup.batchArchivedGroup': '确认归档:{count} 个测试计划组吗',
'testPlan.testPlanGroup.confirmBatchDeletePlanGroup': '确认删除 {count} 个测试计划组吗?',
};

View File

@ -0,0 +1,179 @@
<template>
<a-modal
v-model:visible="innerVisible"
title-align="start"
:title="modelTitle"
body-class="p-0"
:width="600"
:cancel-button-props="{ disabled: confirmLoading }"
:ok-loading="confirmLoading"
:ok-text="t('caseManagement.caseReview.commitResult')"
@before-ok="handleConfirm"
@close="handleCancel"
>
<a-form ref="formRef" :model="form" layout="vertical">
<a-form-item
field="name"
:label="t('testPlan.testPlanGroup.name')"
:rules="[{ required: true, message: t('apiTestDebug.requestNameRequired') }]"
asterisk-position="end"
>
<a-input
v-model:model-value="form.name"
:max-length="255"
:placeholder="t('testPlan.testPlanGroup.planNamePlaceholder')"
/>
</a-form-item>
<a-form-item :label="t('caseManagement.featureCase.ModuleOwned')">
<a-tree-select
v-model:modelValue="form.moduleId"
:data="props.moduleTree"
:field-names="{ title: 'name', key: 'id', children: 'children' }"
:tree-props="{
virtualListProps: {
height: 200,
threshold: 200,
},
}"
allow-search
/>
</a-form-item>
<a-form-item field="tags" :label="t('common.tag')">
<MsTagsInput v-model:modelValue="form.tags"></MsTagsInput>
</a-form-item>
</a-form>
<template #footer>
<a-button type="secondary" @click="handleCancel">{{ t('common.cancel') }}</a-button>
<a-button class="ml-[12px]" type="primary" :loading="confirmLoading" @click="handleConfirm">
{{ okText }}
</a-button>
</template>
</a-modal>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { FormInstance, Message } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import MsTagsInput from '@/components/pure/ms-tags-input/index.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 } from '@/models/testPlan/testPlan';
import { testPlanTypeEnum } from '@/enums/testPlanEnum';
const appStore = useAppStore();
const { t } = useI18n();
const props = defineProps<{
planGroupId?: string;
moduleTree?: ModuleTreeNode[];
moduleId?: string;
}>();
const innerVisible = defineModel<boolean>('visible', {
required: true,
});
const emit = defineEmits<{
(e: 'close'): void;
(e: 'loadPlanList'): void;
}>();
const initPlanGroupForm: AddTestPlanParams = {
groupId: 'NONE',
name: '',
projectId: appStore.currentProjectId,
moduleId: 'root',
cycle: [],
tags: [],
description: '',
testPlanning: false,
automaticStatusUpdate: true,
repeatCase: false,
passThreshold: 100,
type: testPlanTypeEnum.GROUP,
baseAssociateCaseRequest: { selectIds: [], selectAll: false, condition: {} },
};
const form = ref<AddTestPlanParams>(cloneDeep(initPlanGroupForm));
const confirmLoading = ref<boolean>(false);
const formRef = ref<FormInstance>();
function handleCancel() {
innerVisible.value = false;
formRef.value?.resetFields();
form.value = cloneDeep(initPlanGroupForm);
emit('close');
}
function handleConfirm() {
formRef.value?.validate(async (errors) => {
if (!errors) {
confirmLoading.value = true;
try {
const params = {
...cloneDeep(form.value),
groupId: 'NONE',
projectId: appStore.currentProjectId,
type: testPlanTypeEnum.GROUP,
};
if (!props.planGroupId?.length) {
await addTestPlan(params);
Message.success(t('common.createSuccess'));
} else {
await updateTestPlan(params);
Message.success(t('common.updateSuccess'));
}
emit('loadPlanList');
handleCancel();
} catch (error) {
console.log(error);
} finally {
confirmLoading.value = false;
}
}
});
}
const modelTitle = computed(() => {
return props.planGroupId
? t('testPlan.testPlanGroup.updatePlanGroupTitle')
: t('testPlan.testPlanGroup.newPlanGroupTitle');
});
async function getDetail() {
try {
if (props.planGroupId?.length) {
const result = await getTestPlanDetail(props.planGroupId);
form.value = cloneDeep(result);
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
const okText = computed(() => {
return props.planGroupId ? t('common.update') : t('common.create');
});
watch(
() => innerVisible.value,
(val) => {
if (val) {
form.value = cloneDeep(initPlanGroupForm);
getDetail();
form.value.moduleId = props.moduleId && props.moduleId !== 'all' ? props.moduleId : 'root';
}
}
);
</script>
<style scoped></style>