feat(测试计划): 测试计划组联调部分接口&测试计划独立报告页面&测试计划关联用例页面部分和联调部分
This commit is contained in:
parent
4e35887b1e
commit
65a2d5570d
|
@ -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 });
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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]; // 获取模块count分页Api
|
||||
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>
|
|
@ -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',
|
||||
};
|
|
@ -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': '自动添加已关联的接口用例',
|
||||
};
|
|
@ -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>
|
|
@ -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 {};
|
|
@ -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 {};
|
|
@ -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 {};
|
|
@ -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 {};
|
|
@ -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',
|
||||
};
|
||||
|
|
|
@ -186,5 +186,6 @@ export default {
|
|||
'common.updateTime': '更新时间',
|
||||
'common.belongProject': '所属项目',
|
||||
'common.noMatchData': '暂无匹配数据',
|
||||
'common.name': '名称',
|
||||
'common.stopped': '已停止',
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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': '请选择过滤条件',
|
||||
|
|
|
@ -53,6 +53,7 @@
|
|||
label: 'common.fakeError',
|
||||
},
|
||||
DEFAULT: {
|
||||
icon: '',
|
||||
label: '-',
|
||||
color: '!text-[var(--color-text-input-border)]',
|
||||
},
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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',
|
||||
};
|
||||
|
|
|
@ -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': '一键填写报告总结',
|
||||
};
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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?',
|
||||
};
|
||||
|
|
|
@ -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} 个测试计划组吗?',
|
||||
};
|
||||
|
|
|
@ -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>
|
Loading…
Reference in New Issue