feat(测试计划): 测试计划联调执行历史&补充权限&用例详情缺陷新建和关联&调整bug细节问题

This commit is contained in:
xinxin.wu 2024-05-17 17:37:09 +08:00 committed by Craftsman
parent d96d4fc8ac
commit a66ba037f5
24 changed files with 522 additions and 272 deletions

View File

@ -8,6 +8,7 @@ import {
batchCopyPlanUrl, batchCopyPlanUrl,
batchDeletePlanUrl, batchDeletePlanUrl,
BatchDisassociateCaseUrl, BatchDisassociateCaseUrl,
BatchEditTestPlanUrl,
batchMovePlanUrl, batchMovePlanUrl,
BatchRunCaseUrl, BatchRunCaseUrl,
BatchUpdateCaseExecutorUrl, BatchUpdateCaseExecutorUrl,
@ -16,6 +17,7 @@ import {
DeleteTestPlanModuleUrl, DeleteTestPlanModuleUrl,
DisassociateCaseUrl, DisassociateCaseUrl,
EditCaseLastExecResultUrl, EditCaseLastExecResultUrl,
ExecuteHistoryUrl,
followPlanUrl, followPlanUrl,
GenerateReportUrl, GenerateReportUrl,
GetAssociatedBugUrl, GetAssociatedBugUrl,
@ -52,6 +54,8 @@ import type {
BatchUpdateCaseExecutorParams, BatchUpdateCaseExecutorParams,
DisassociateCaseParams, DisassociateCaseParams,
EditLastExecResultParams, EditLastExecResultParams,
ExecuteHistoryItem,
ExecuteHistoryType,
FollowPlanParams, FollowPlanParams,
PassRateCountDetail, PassRateCountDetail,
PlanDetailBugItem, PlanDetailBugItem,
@ -85,6 +89,11 @@ export function moveTestPlanModuleTree(data: MoveModules) {
return MSR.post({ url: MoveTestPlanModuleUrl, data }); return MSR.post({ url: MoveTestPlanModuleUrl, data });
} }
// 批量编辑测试计划
export function batchEditTestPlan(data: TableQueryParams) {
return MSR.post({ url: BatchEditTestPlanUrl, data });
}
// 删除模块 // 删除模块
export function deletePlanModuleTree(id: string) { export function deletePlanModuleTree(id: string) {
return MSR.get({ url: `${DeleteTestPlanModuleUrl}/${id}` }); return MSR.get({ url: `${DeleteTestPlanModuleUrl}/${id}` });
@ -226,3 +235,7 @@ export function associateBugToPlan(data: TableQueryParams) {
export function testPlanCancelBug(id: string) { export function testPlanCancelBug(id: string) {
return MSR.get({ url: `${TestPlanCancelBugUrl}/${id}` }); return MSR.get({ url: `${TestPlanCancelBugUrl}/${id}` });
} }
// 测试计划-用例详情-执行历史
export function executeHistory(data: ExecuteHistoryType) {
return MSR.post<ExecuteHistoryItem[]>({ url: ExecuteHistoryUrl, data });
}

View File

@ -22,6 +22,8 @@ export const UpdateTestPlanUrl = '/test-plan/update';
export const batchDeletePlanUrl = '/test-plan/batch-delete'; export const batchDeletePlanUrl = '/test-plan/batch-delete';
// 删除测试计划 // 删除测试计划
export const deletePlanUrl = '/test-plan/delete'; export const deletePlanUrl = '/test-plan/delete';
// 测试计划批量编辑
export const BatchEditTestPlanUrl = '/test-plan/batch-edit';
// 获取统计数量 // 获取统计数量
export const getStatisticalCountUrl = '/test-plan/getCount'; export const getStatisticalCountUrl = '/test-plan/getCount';
// 归档 // 归档
@ -29,7 +31,7 @@ export const archivedPlanUrl = '/test-plan/archived';
// 批量复制 // 批量复制
export const batchCopyPlanUrl = '/test-plan/batch-copy'; export const batchCopyPlanUrl = '/test-plan/batch-copy';
// 批量移动 // 批量移动
export const batchMovePlanUrl = '/test-plan/batch/move'; export const batchMovePlanUrl = '/test-plan/batch-move';
// 批量归档 // 批量归档
export const batchArchivedPlanUrl = '/test-plan/batch-archived'; export const batchArchivedPlanUrl = '/test-plan/batch-archived';
// 计划详情缺陷管理列表 // 计划详情缺陷管理列表
@ -74,3 +76,5 @@ export const BatchRunCaseUrl = '/test-plan/functional/case/batch/run';
export const GetTestPlanUsersUrl = '/test-plan/functional/case/user-option'; export const GetTestPlanUsersUrl = '/test-plan/functional/case/user-option';
// 计划详情-功能用例-批量更新执行人 // 计划详情-功能用例-批量更新执行人
export const BatchUpdateCaseExecutorUrl = '/test-plan/functional/case/batch/update/executor'; export const BatchUpdateCaseExecutorUrl = '/test-plan/functional/case/batch/update/executor';
// 计划详情-功能用例-执行历史
export const ExecuteHistoryUrl = '/test-plan/functional/case/exec/history';

View File

@ -86,7 +86,7 @@
}" }"
:expand-all="isExpandAll" :expand-all="isExpandAll"
block-node block-node
title-tooltip-position="left" title-tooltip-position="top"
@select="folderNodeSelect" @select="folderNodeSelect"
> >
<template #title="nodeData"> <template #title="nodeData">
@ -212,9 +212,11 @@
moduleCountParams?: TableQueryParams; // moduleCountParams?: TableQueryParams; //
hideProjectSelect?: boolean; // hideProjectSelect?: boolean; //
isHiddenCaseLevel?: boolean; isHiddenCaseLevel?: boolean;
selectorAll: boolean;
}>(), }>(),
{ {
isHiddenCaseLevel: false, isHiddenCaseLevel: false,
selectorAll: false,
} }
); );
@ -421,6 +423,7 @@
selectable: true, selectable: true,
showSelectAll: true, showSelectAll: true,
heightUsed: 310, heightUsed: 310,
showSelectorAll: !props.selectorAll,
}, },
(record) => { (record) => {
return { return {

View File

@ -185,6 +185,8 @@ export interface RunFeatureCaseParams extends ExecuteFeatureCaseFormParams {
notifier?: string; notifier?: string;
} }
export type ExecuteHistoryType = Pick<RunFeatureCaseParams, 'id' | 'testPlanId' | 'caseId'>;
export interface BatchExecuteFeatureCaseParams extends BatchFeatureCaseParams, ExecuteFeatureCaseFormParams { export interface BatchExecuteFeatureCaseParams extends BatchFeatureCaseParams, ExecuteFeatureCaseFormParams {
notifier?: string; notifier?: string;
} }
@ -213,4 +215,19 @@ export interface PassRateCountDetail {
apiScenarioCount: number; apiScenarioCount: number;
} }
// 执行历史
export interface ExecuteHistoryItem {
status: string;
content: string;
contentText: string;
stepsExecResult: string;
createUser: string;
userName: string;
userLogo: string;
email: string;
steps: string;
createTime: string;
deleted: boolean;
}
export default {}; export default {};

View File

@ -65,7 +65,7 @@
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
import type { BatchEditCaseType, CustomAttributes } from '@/models/caseManagement/featureCase'; import type { CustomAttributes } from '@/models/caseManagement/featureCase';
import { TableQueryParams } from '@/models/common'; import { TableQueryParams } from '@/models/common';
import Message from '@arco-design/web-vue/es/message'; import Message from '@arco-design/web-vue/es/message';

View File

@ -25,7 +25,7 @@
children: 'children', children: 'children',
count: 'count', count: 'count',
}" }"
title-tooltip-position="left" title-tooltip-position="top"
@select="caseNodeSelect" @select="caseNodeSelect"
@more-action-select="handleCaseMoreSelect" @more-action-select="handleCaseMoreSelect"
@more-actions-close="moreActionsClose" @more-actions-close="moreActionsClose"

View File

@ -97,10 +97,10 @@
}); });
const innerKeyword = useVModel(props, 'keyword', emit); const innerKeyword = useVModel(props, 'keyword', emit);
function searchData() { function searchData(keyword?: string) {
setLinkListParams({ setLinkListParams({
...props.loadParams, ...props.loadParams,
keyword: innerKeyword.value, keyword,
projectId: appStore.currentProjectId, projectId: appStore.currentProjectId,
condition: { condition: {
keyword: innerKeyword.value, keyword: innerKeyword.value,
@ -123,7 +123,7 @@
} }
onBeforeMount(() => { onBeforeMount(() => {
searchData(); searchData(innerKeyword.value);
}); });
watch( watch(
@ -139,7 +139,7 @@
() => props.caseId, () => props.caseId,
(val) => { (val) => {
if (val) { if (val) {
searchData(); searchData(innerKeyword.value);
} }
} }
); );

View File

@ -57,7 +57,6 @@
import { TableKeyEnum } from '@/enums/tableEnum'; import { TableKeyEnum } from '@/enums/tableEnum';
import { getCaseLevels } from '@/views/case-management/caseManagementFeature/components/utils';
import debounce from 'lodash-es/debounce'; import debounce from 'lodash-es/debounce';
const { t } = useI18n(); const { t } = useI18n();

View File

@ -73,19 +73,19 @@
/> />
<ms-base-table v-else v-bind="testPlanPropsRes" ref="planTableRef" v-on="testPlanTableEvent"> <ms-base-table v-else v-bind="testPlanPropsRes" ref="planTableRef" v-on="testPlanTableEvent">
<template #name="{ record }"> <template #name="{ record }">
<div class="one-line-text max-w-[300px]"> {{ record.name }}</div> <div class="flex flex-nowrap items-center">
<a-popover title="" position="right"> <div class="one-line-text">{{ characterLimit(record.name) }}</div>
<span class="ml-1 text-[rgb(var(--primary-5))]">{{ t('caseManagement.featureCase.preview') }}</span> <a-popover title="" position="right" style="width: 480px">
<template #content> <div class="ml-1 text-[rgb(var(--primary-5))]">{{ t('caseManagement.featureCase.preview') }}</div>
<div class="max-w-[600px] text-[14px] text-[var(--color-text-1)]"> <template #content>
{{ record.content }} <div v-dompurify-html="record.content" class="markdown-body" style="margin-left: 48px"> </div>
</div> </template>
</template> </a-popover>
</a-popover> </div>
</template> </template>
<template #handleUserName="{ record }"> <template #handleUserName="{ record }">
<a-tooltip :content="record.handleUserName"> <a-tooltip :content="record.handleUserName">
<div class="one-line-text max-w-[200px]">{{ characterLimit(record.handleUserName) }}</div> <div class="one-line-text max-w-[200px]">{{ characterLimit(record.handleUserName) || '-' }}</div>
</a-tooltip> </a-tooltip>
</template> </template>
<template #testPlanName="{ record }"> <template #testPlanName="{ record }">
@ -259,7 +259,7 @@
{ {
title: 'caseManagement.featureCase.tableColumnID', title: 'caseManagement.featureCase.tableColumnID',
dataIndex: 'num', dataIndex: 'num',
width: 200, width: 100,
showInTable: true, showInTable: true,
showTooltip: true, showTooltip: true,
ellipsis: true, ellipsis: true,
@ -271,7 +271,7 @@
dataIndex: 'name', dataIndex: 'name',
showInTable: true, showInTable: true,
showTooltip: true, showTooltip: true,
width: 300, width: 250,
ellipsis: true, ellipsis: true,
showDrag: false, showDrag: false,
}, },
@ -281,7 +281,7 @@
dataIndex: 'testPlanName', dataIndex: 'testPlanName',
showInTable: true, showInTable: true,
showTooltip: true, showTooltip: true,
width: 300, width: 200,
ellipsis: true, ellipsis: true,
showDrag: false, showDrag: false,
}, },
@ -291,7 +291,7 @@
dataIndex: 'defectState', dataIndex: 'defectState',
showInTable: true, showInTable: true,
showTooltip: true, showTooltip: true,
width: 300, width: 150,
ellipsis: true, ellipsis: true,
showDrag: false, showDrag: false,
}, },
@ -305,7 +305,7 @@
}, },
showInTable: true, showInTable: true,
showTooltip: true, showTooltip: true,
width: 300, width: 200,
ellipsis: true, ellipsis: true,
}, },
]; ];
@ -364,7 +364,7 @@
return; return;
} }
if (showType.value === 'link') { if (showType.value === 'link') {
bugTableListRef.value?.searchData(); bugTableListRef.value?.searchData(keyword.value);
} else { } else {
setTestPlanListParams(initTableParams()); setTestPlanListParams(initTableParams());
await testPlanLinkList(); await testPlanLinkList();
@ -373,7 +373,7 @@
} }
async function resetFetch() { async function resetFetch() {
if (showType.value === 'link') { if (showType.value === 'link') {
bugTableListRef.value?.searchData(); bugTableListRef.value?.searchData(keyword.value);
} else { } else {
setTestPlanListParams({ keyword: '', projectId: appStore.currentProjectId, testPlanCaseId: props.caseId }); setTestPlanListParams({ keyword: '', projectId: appStore.currentProjectId, testPlanCaseId: props.caseId });
await testPlanLinkList(); await testPlanLinkList();

View File

@ -45,7 +45,7 @@
{{ t('caseManagement.caseReview.fail') }} {{ t('caseManagement.caseReview.fail') }}
</div> </div>
<div v-else-if="item.status === 'UNDER_REVIEWED'" class="flex items-center"> <div v-else-if="item.status === 'UNDER_REVIEWED'" class="flex items-center">
<MsIcon type="icon-icon_warning_filled" class="mr-[4px] text-[rgb(var(--warning-6))]" /> <MsIcon type="icon-icon_warning_filled" class="mr-[4px] text-[rgb(var(--link-6))]" />
{{ t('caseManagement.caseReview.suggestion') }} {{ t('caseManagement.caseReview.suggestion') }}
</div> </div>
<div v-else-if="item.status === 'RE_REVIEWED'" class="flex items-center"> <div v-else-if="item.status === 'RE_REVIEWED'" class="flex items-center">
@ -57,11 +57,11 @@
{{ t('caseManagement.featureCase.execute.success') }} {{ t('caseManagement.featureCase.execute.success') }}
</div> </div>
<div v-if="item.status === 'BLOCKED'" class="flex items-center"> <div v-if="item.status === 'BLOCKED'" class="flex items-center">
<MsIcon type="icon-icon_succeed_filled" class="mr-[4px] text-[rgb(var(--success-6))]" /> <MsIcon type="icon-icon_succeed_filled" class="mr-[4px] text-[rgb(var(--warning-6))]" />
{{ t('caseManagement.featureCase.execute.blocked') }} {{ t('caseManagement.featureCase.execute.blocked') }}
</div> </div>
<div v-if="item.status === 'FAILED'" class="flex items-center"> <div v-if="item.status === 'FAILED'" class="flex items-center">
<MsIcon type="icon-icon_succeed_filled" class="mr-[4px] text-[rgb(var(--success-6))]" /> <MsIcon type="icon-icon_succeed_filled" class="mr-[4px] text-[rgb(var(--danger-6))]" />
{{ t('caseManagement.featureCase.execute.failed') }} {{ t('caseManagement.featureCase.execute.failed') }}
</div> </div>
</div> </div>

View File

@ -10,23 +10,30 @@
:type="RequestModuleEnum.CASE_MANAGEMENT" :type="RequestModuleEnum.CASE_MANAGEMENT"
hide-project-select hide-project-select
is-hidden-case-level is-hidden-case-level
:selector-all="true"
@save="saveHandler" @save="saveHandler"
> >
</MsCaseAssociate> </MsCaseAssociate>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useRoute } from 'vue-router';
import { Message } from '@arco-design/web-vue';
import MsCaseAssociate from '@/components/business/ms-case-associate/index.vue'; import MsCaseAssociate from '@/components/business/ms-case-associate/index.vue';
import { RequestModuleEnum } from '@/components/business/ms-case-associate/utils'; import { RequestModuleEnum } from '@/components/business/ms-case-associate/utils';
import { getCaseList, getCaseModuleTree } from '@/api/modules/case-management/featureCase'; import { getCaseList, getCaseModuleTree } from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
import type { AssociateCaseRequest } from '@/models/testPlan/testPlan'; import type { AssociateCaseRequest, AssociateCaseRequestType } from '@/models/testPlan/testPlan';
import { CaseLinkEnum } from '@/enums/caseEnum'; import { CaseLinkEnum } from '@/enums/caseEnum';
const { t } = useI18n();
const props = defineProps<{ const props = defineProps<{
hasNotAssociatedIds?: string[]; hasNotAssociatedIds?: string[];
saveApi?: (params: AssociateCaseRequestType) => Promise<any>;
}>(); }>();
const innerVisible = defineModel<boolean>('visible', { const innerVisible = defineModel<boolean>('visible', {
required: true, required: true,
@ -36,16 +43,31 @@
}>(); }>();
const appStore = useAppStore(); const appStore = useAppStore();
const route = useRoute();
const currentSelectCase = ref<keyof typeof CaseLinkEnum>('FUNCTIONAL'); const currentSelectCase = ref<keyof typeof CaseLinkEnum>('FUNCTIONAL');
const currentProjectId = ref(appStore.currentProjectId); const currentProjectId = ref(appStore.currentProjectId);
const confirmLoading = ref<boolean>(false); const confirmLoading = ref<boolean>(false);
const planId = ref(route.query.id as string);
function saveHandler(params: AssociateCaseRequest) { async function saveHandler(params: AssociateCaseRequest) {
try { try {
confirmLoading.value = true; confirmLoading.value = true;
emit('success', { ...params, functionalSelectIds: params.selectIds }); if (typeof props.saveApi !== 'function') {
emit('success', { ...params, functionalSelectIds: params.selectIds });
} else {
try {
await props.saveApi({
functionalSelectIds: params.selectIds,
testPlanId: planId.value,
});
emit('success', { ...params, functionalSelectIds: params.selectIds });
Message.success(t('ms.case.associate.associateSuccess'));
confirmLoading.value = false;
} catch (error) {
console.log(error);
}
}
innerVisible.value = false; innerVisible.value = false;
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console

View File

@ -1,100 +1,116 @@
<template> <template>
<MsDialog <a-modal v-model:visible="isVisible" title-align="start" class="ms-modal-upload ms-modal-medium" :width="480">
v-model:visible="isVisible" <template #title>
dialog-size="small" {{ t('common.edit') }}
:title="t('testPlan.testPlanIndex.batchEdit', { number: props.batchParams.currentSelectCount })" <div class="text-[var(--color-text-4)]">
ok-text="common.update" {{
:confirm="confirmHandler" t('case.batchModalSubTitle', {
:close="closeHandler" count: props.batchParams.currentSelectCount,
unmount-on-close })
:switch-props="{ }}
switchName: t('caseManagement.featureCase.appendTag'), </div>
switchTooltip: t('caseManagement.featureCase.enableTags'), </template>
showSwitch: form.selectedAttrsId === 'tags' ? true : false, <a-form ref="formRef" class="rounded-[4px]" :model="form" layout="vertical">
enable: form.append, <a-form-item
}" field="selectedAttrsId"
> :label="t('apiTestManagement.chooseAttr')"
<div class="form"> :rules="[{ required: true, message: t('apiTestManagement.attrRequired') }]"
<a-form ref="formRef" class="rounded-[4px]" :model="form" layout="vertical"> asterisk-position="end"
<a-form-item >
field="selectedAttrsId" <a-select v-model="form.selectedAttrsId" :placeholder="t('common.pleaseSelect')">
:label="t('apiTestManagement.chooseAttr')" <a-option v-for="item of attrOptions" :key="item.value" :value="item.value">
:rules="[{ required: true, message: t('apiTestManagement.attrRequired') }]" {{ t(item.name) }}
asterisk-position="end" </a-option>
> </a-select>
<a-select v-model="form.selectedAttrsId" :placeholder="t('common.pleaseSelect')"> </a-form-item>
<a-option v-for="item of attrOptions" :key="item.value" :value="item.value"> <a-form-item
{{ t(item.name) }} v-if="form.selectedAttrsId === 'tags'"
</a-option> field="tags"
</a-select> :label="t('apiTestManagement.batchUpdate')"
</a-form-item> :rules="[{ required: true, message: t('apiTestManagement.valueRequired') }]"
<a-form-item asterisk-position="end"
class="mb-0"
required
>
<MsTagsInput v-model:modelValue="form.tags" allow-clear></MsTagsInput>
</a-form-item>
<a-form-item
v-else
field="value"
:label="t('apiTestManagement.batchUpdate')"
:rules="[{ required: true, message: t('apiTestManagement.valueRequired') }]"
asterisk-position="end"
class="mb-0"
>
<a-select v-model="form.value" :placeholder="t('common.pleaseSelect')" :disabled="form.selectedAttrsId === ''">
<a-option v-for="item of valueOptions" :key="item.value" :value="item.value">
{{ t(item.label) }}
</a-option>
</a-select>
</a-form-item>
</a-form>
<template #footer>
<div class="flex" :class="[form.selectedAttrsId === 'tags' ? 'justify-between' : 'justify-end']">
<div
v-if="form.selectedAttrsId === 'tags'" v-if="form.selectedAttrsId === 'tags'"
field="values" class="flex flex-row items-center justify-center"
:label="t('apiTestManagement.batchUpdate')" style="padding-top: 10px"
:validate-trigger="['blur', 'input']"
:rules="[{ required: true, message: t('apiTestManagement.valueRequired') }]"
asterisk-position="end"
class="mb-0"
required
> >
<MsTagsInput <a-switch v-model="form.append" class="mr-1" size="small" type="line" />
v-model:model-value="form.tags" <span class="flex items-center">
placeholder="common.tagsInputPlaceholder" <span class="mr-1">{{ t('caseManagement.featureCase.appendTag') }}</span>
allow-clear <span class="mt-[2px]">
unique-value <a-tooltip>
retain-input-value <IconQuestionCircle class="h-[16px] w-[16px] text-[rgb(var(--primary-5))]" />
/> <template #content>
</a-form-item> <div>{{ t('caseManagement.featureCase.enableTags') }}</div>
<a-form-item <div>{{ t('caseManagement.featureCase.closeTags') }}</div>
v-else </template>
field="value" </a-tooltip>
:label="t('apiTestManagement.batchUpdate')" </span>
:rules="[{ required: true, message: t('apiTestManagement.valueRequired') }]" </span>
asterisk-position="end" </div>
class="mb-0" <div class="flex justify-end">
> <a-button type="secondary" :disabled="batchEditLoading" @click="closeHandler">
<a-select {{ t('common.cancel') }}
v-model="form.value" </a-button>
:placeholder="t('common.pleaseSelect')" <a-button class="ml-3" type="primary" :loading="batchEditLoading" @click="confirmHandler">
:disabled="form.selectedAttrsId === ''" {{ t('common.update') }}
> </a-button>
<a-option v-for="item of valueOptions" :key="item.value" :value="item.value"> </div>
{{ t(item.label) }} </div>
</a-option> </template>
</a-select> </a-modal>
</a-form-item>
</a-form>
</div>
</MsDialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
import { FormInstance, ValidatedError } from '@arco-design/web-vue'; import { FormInstance } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es'; import { cloneDeep } from 'lodash-es';
import MsDialog from '@/components/pure/ms-dialog/index.vue';
import type { BatchActionQueryParams } from '@/components/pure/ms-table/type'; import type { BatchActionQueryParams } from '@/components/pure/ms-table/type';
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue'; import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
import { batchEditTestPlan } from '@/api/modules/test-plan/testPlan';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
import { TableQueryParams } from '@/models/common'; import { TableQueryParams } from '@/models/common';
import { testPlanTypeEnum } from '@/enums/testPlanEnum';
import Message from '@arco-design/web-vue/es/message'; import Message from '@arco-design/web-vue/es/message';
const isVisible = ref<boolean>(false); const isVisible = ref<boolean>(false);
const appStore = useAppStore();
const { t } = useI18n(); const { t } = useI18n();
const appStore = useAppStore();
const props = defineProps<{ const props = defineProps<{
visible: boolean; visible: boolean;
batchParams: BatchActionQueryParams; batchParams: BatchActionQueryParams;
activeFolder: string; activeFolder: string;
offspringIds: string[]; offspringIds: string[];
condition?: TableQueryParams; condition?: TableQueryParams;
showType: keyof typeof testPlanTypeEnum;
}>(); }>();
const emits = defineEmits<{ const emits = defineEmits<{
@ -102,7 +118,6 @@
(e: 'success'): void; (e: 'success'): void;
}>(); }>();
const currentProjectId = computed(() => appStore.currentProjectId);
const initForm = { const initForm = {
selectedAttrsId: '', selectedAttrsId: '',
append: false, append: false,
@ -126,33 +141,33 @@
form.value = { ...initForm }; form.value = { ...initForm };
} }
async function confirmHandler(enable: boolean | undefined) { const batchEditLoading = ref(false);
formRef.value?.validate(async (errors: undefined | Record<string, ValidatedError>) => { function confirmHandler() {
formRef.value?.validate(async (errors) => {
if (!errors) { if (!errors) {
try { try {
const customField = { batchEditLoading.value = true;
fieldId: '',
value: '',
};
const { selectedIds, selectAll, excludeIds } = props.batchParams; const { selectedIds, selectAll, excludeIds } = props.batchParams;
const params: TableQueryParams = { const params: TableQueryParams = {
selectIds: selectedIds || [], selectIds: selectedIds || [],
selectAll: !!selectAll, selectAll: !!selectAll,
excludeIds: excludeIds || [], excludeIds: excludeIds || [],
projectId: currentProjectId.value, projectId: appStore.currentProjectId,
append: enable as boolean,
tags: form.value.tags,
moduleIds: props.activeFolder === 'all' ? [] : [props.activeFolder, ...props.offspringIds], moduleIds: props.activeFolder === 'all' ? [] : [props.activeFolder, ...props.offspringIds],
customField: form.value.selectedAttrsId === 'systemTags' ? {} : customField,
condition: { condition: {
...props.condition, ...props.condition,
}, },
...form.value,
type: props.showType,
}; };
await batchEditTestPlan(params);
Message.success(t('caseManagement.featureCase.editSuccess')); Message.success(t('caseManagement.featureCase.editSuccess'));
closeHandler(); closeHandler();
emits('success'); emits('success');
} catch (e) { } catch (e) {
console.log(e); console.log(e);
} finally {
batchEditLoading.value = false;
} }
} else { } else {
return false; return false;

View File

@ -44,6 +44,7 @@
class="mt-4" class="mt-4"
:action-config="testPlanBatchActions" :action-config="testPlanBatchActions"
filter-icon-align-left filter-icon-align-left
:selectable="hasOperationPermission"
v-on="propsEvent" v-on="propsEvent"
@batch-action="handleTableBatch" @batch-action="handleTableBatch"
> >
@ -115,7 +116,7 @@
<StatusProgress :status-detail="defaultCountDetailMap[record.id]" height="5px" /> <StatusProgress :status-detail="defaultCountDetailMap[record.id]" height="5px" />
</div> </div>
<div class="text-[var(--color-text-1)]"> <div class="text-[var(--color-text-1)]">
{{ `${record.passRate || 0}%` }} {{ `${defaultCountDetailMap[record.id] ? defaultCountDetailMap[record.id].passRate : '-'}%` }}
</div> </div>
</template> </template>
<template #passRateTitleSlot="{ columnConfig }"> <template #passRateTitleSlot="{ columnConfig }">
@ -173,10 +174,17 @@
<template #operation="{ record }"> <template #operation="{ record }">
<div class="flex items-center"> <div class="flex items-center">
<MsButton v-if="record.functionalCaseCount > 0" class="!mx-0">{{ <MsButton
t('testPlan.testPlanIndex.execution') v-if="record.functionalCaseCount > 0 && hasAnyPermission(['PROJECT_TEST_PLAN:READ+EXECUTE'])"
}}</MsButton> class="!mx-0"
<a-divider v-if="record.functionalCaseCount > 0" direction="vertical" :margin="8"></a-divider> @click="openDetail(record.id)"
>{{ t('testPlan.testPlanIndex.execution') }}</MsButton
>
<a-divider
v-if="record.functionalCaseCount > 0 && hasAnyPermission(['PROJECT_TEST_PLAN:READ+EXECUTE'])"
direction="vertical"
:margin="8"
></a-divider>
<MsButton <MsButton
v-permission="['PROJECT_TEST_PLAN:READ+UPDATE']" v-permission="['PROJECT_TEST_PLAN:READ+UPDATE']"
@ -245,6 +253,7 @@
:active-folder="props.activeFolder" :active-folder="props.activeFolder"
:offspring-ids="props.offspringIds" :offspring-ids="props.offspringIds"
:condition="conditionParams" :condition="conditionParams"
:show-type="showType"
@success="successHandler" @success="successHandler"
/> />
</template> </template>
@ -289,6 +298,7 @@
import { characterLimit } from '@/utils'; import { characterLimit } from '@/utils';
import { hasAnyPermission } from '@/utils/permission'; import { hasAnyPermission } from '@/utils/permission';
import type { TableQueryParams } from '@/models/common';
import { ModuleTreeNode } from '@/models/common'; import { ModuleTreeNode } from '@/models/common';
import type { PassRateCountDetail, planStatusType, TestPlanItem } from '@/models/testPlan/testPlan'; import type { PassRateCountDetail, planStatusType, TestPlanItem } from '@/models/testPlan/testPlan';
import { TestPlanRouteEnum } from '@/enums/routeEnum'; import { TestPlanRouteEnum } from '@/enums/routeEnum';
@ -319,6 +329,10 @@
(e: 'editOrCopy', id: string, isCopy: boolean): void; (e: 'editOrCopy', id: string, isCopy: boolean): void;
}>(); }>();
const hasOperationPermission = computed(() =>
hasAnyPermission(['PROJECT_TEST_PLAN:READ+UPDATE', 'PROJECT_TEST_PLAN:READ+EXECUTE', 'PROJECT_TEST_PLAN:READ+ADD'])
);
const columns: MsTableColumn = [ const columns: MsTableColumn = [
{ {
title: 'testPlan.testPlanIndex.ID', title: 'testPlan.testPlanIndex.ID',
@ -443,11 +457,11 @@
showDrag: true, showDrag: true,
}, },
{ {
title: 'testPlan.testPlanIndex.operation', title: hasOperationPermission.value ? 'testPlan.testPlanIndex.operation' : '',
slotName: 'operation', slotName: 'operation',
dataIndex: 'operation', dataIndex: 'operation',
fixed: 'right', fixed: 'right',
width: 200, width: hasOperationPermission.value ? 200 : 50,
showInTable: true, showInTable: true,
showDrag: false, showDrag: false,
}, },
@ -537,37 +551,43 @@
{ {
label: 'common.archive', label: 'common.archive',
eventTag: 'archive', eventTag: 'archive',
permission: ['PROJECT_TEST_PLAN:READ+UPDATE'],
}, },
]; ];
const copyActions: ActionsItem[] = [ const copyActions: ActionsItem[] = [
{ {
label: 'common.copy', label: 'common.copy',
eventTag: 'copy', eventTag: 'copy',
permission: ['PROJECT_TEST_PLAN:READ+ADD'],
}, },
]; ];
function getMoreActions(status: planStatusType, useCount: number) { function getMoreActions(status: planStatusType, useCount: number) {
//
const copyAction = useCount > 0 ? copyActions : []; const copyAction = useCount > 0 ? copyActions : [];
if (status === 'COMPLETED' || status === 'ARCHIVED') { //
if (status === 'ARCHIVED' || status === 'PREPARED' || status === 'UNDERWAY') {
return [ return [
...copyAction, ...copyAction,
{
isDivider: true,
},
{ {
label: 'common.delete', label: 'common.delete',
danger: true, danger: true,
eventTag: 'delete', eventTag: 'delete',
permission: ['PROJECT_TEST_PLAN:READ+DELETE'],
}, },
]; ];
} }
return [ return [
...copyAction, ...copyAction,
...archiveActions, ...archiveActions,
{
isDivider: true,
},
{ {
label: 'common.delete', label: 'common.delete',
danger: true, danger: true,
eventTag: 'delete', eventTag: 'delete',
permission: ['PROJECT_TEST_PLAN:READ+DELETE'],
}, },
]; ];
} }
@ -610,7 +630,6 @@
filter: propsRes.value.filter, filter: propsRes.value.filter,
combine: batchParams.value.condition, combine: batchParams.value.condition,
}; };
return { return {
type: showType.value, type: showType.value,
moduleIds: props.activeFolder && props.activeFolder !== 'all' ? [props.activeFolder, ...props.offspringIds] : [], moduleIds: props.activeFolder && props.activeFolder !== 'all' ? [props.activeFolder, ...props.offspringIds] : [],
@ -620,6 +639,7 @@
selectIds: batchParams.value.selectedIds || [], selectIds: batchParams.value.selectedIds || [],
keyword: keyword.value, keyword: keyword.value,
condition: { condition: {
filter: propsRes.value.filter,
keyword: keyword.value, keyword: keyword.value,
}, },
combine: { combine: {
@ -832,6 +852,7 @@
} }
function successHandler() { function successHandler() {
resetSelector();
fetchData(); fetchData();
} }

View File

@ -8,7 +8,7 @@
:node-more-actions="caseMoreActions" :node-more-actions="caseMoreActions"
:expand-all="props.isExpandAll" :expand-all="props.isExpandAll"
:empty-text="t('testPlan.testPlanIndex.planEmptyContent')" :empty-text="t('testPlan.testPlanIndex.planEmptyContent')"
draggable :draggable="hasAnyPermission(['PROJECT_TEST_PLAN:READ+UPDATE'])"
:virtual-list-props="virtualListProps" :virtual-list-props="virtualListProps"
block-node block-node
:field-names="{ :field-names="{
@ -17,7 +17,7 @@
children: 'children', children: 'children',
count: 'count', count: 'count',
}" }"
title-tooltip-position="left" title-tooltip-position="top"
@select="planNodeSelect" @select="planNodeSelect"
@more-action-select="handlePlanMoreSelect" @more-action-select="handlePlanMoreSelect"
@more-actions-close="moreActionsClose" @more-actions-close="moreActionsClose"
@ -33,6 +33,7 @@
</template> </template>
<template #extra="nodeData"> <template #extra="nodeData">
<MsPopConfirm <MsPopConfirm
v-if="hasAnyPermission(['PROJECT_TEST_PLAN:READ+ADD'])"
:visible="addSubVisible" :visible="addSubVisible"
:is-delete="false" :is-delete="false"
:all-names="[]" :all-names="[]"
@ -50,6 +51,7 @@
</MsButton> </MsButton>
</MsPopConfirm> </MsPopConfirm>
<MsPopConfirm <MsPopConfirm
v-if="hasAnyPermission(['PROJECT_TEST_PLAN:READ+UPDATE'])"
:title="t('testPlan.testPlanIndex.rename')" :title="t('testPlan.testPlanIndex.rename')"
:all-names="[]" :all-names="[]"
:is-delete="false" :is-delete="false"
@ -87,6 +89,7 @@
import useModal from '@/hooks/useModal'; import useModal from '@/hooks/useModal';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
import { mapTree } from '@/utils'; import { mapTree } from '@/utils';
import { hasAnyPermission } from '@/utils/permission';
import type { CreateOrUpdateModule, UpdateModule } from '@/models/caseManagement/featureCase'; import type { CreateOrUpdateModule, UpdateModule } from '@/models/caseManagement/featureCase';
import { ModuleTreeNode } from '@/models/common'; import { ModuleTreeNode } from '@/models/common';

View File

@ -287,9 +287,12 @@
if (props.planId?.length) { if (props.planId?.length) {
const result = await getTestPlanDetail(props.planId); const result = await getTestPlanDetail(props.planId);
form.value = cloneDeep(result); form.value = cloneDeep(result);
let copyName = `copy_${result.name}`; if (props.isCopy) {
copyName = copyName.length > 255 ? copyName.slice(0, 255) : copyName; let copyName = `copy_${result.name}`;
form.value.name = copyName; 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.cycle = [result.plannedStartTime as number, result.plannedEndTime as number];
} }
} catch (error) { } catch (error) {

View File

@ -534,6 +534,7 @@
...tableParams.value, ...tableParams.value,
...batchExecuteForm.value, ...batchExecuteForm.value,
notifier: batchExecuteForm.value?.commentIds?.join(';'), notifier: batchExecuteForm.value?.commentIds?.join(';'),
selectIds: batchParams.value.selectedIds,
}); });
Message.success(t('common.updateSuccess')); Message.success(t('common.updateSuccess'));
resetSelector(); resetSelector();
@ -636,6 +637,7 @@
defineExpose({ defineExpose({
resetSelector, resetSelector,
loadCaseList,
}); });
await tableStore.initColumn(TableKeyEnum.TEST_PLAN_DETAIL_FEATURE_CASE_TABLE, columns, 'drawer', true); await tableStore.initColumn(TableKeyEnum.TEST_PLAN_DETAIL_FEATURE_CASE_TABLE, columns, 'drawer', true);

View File

@ -48,7 +48,7 @@
class="mx-[8px] w-[240px]" class="mx-[8px] w-[240px]"
@search="initData" @search="initData"
@press-enter="initData" @press-enter="initData"
@clear="initData" @clear="resetHandler"
/> />
</div> </div>
<BugList <BugList
@ -62,26 +62,10 @@
testPlanCaseId: route.query.testPlanCaseId, testPlanCaseId: route.query.testPlanCaseId,
caseId: props.caseId, caseId: props.caseId,
}" }"
@link="linkDefect" @link="emit('link')"
@new="createDefect" @new="emit('new')"
@cancel-link="cancelLink" @cancel-link="cancelLink"
/> />
<LinkDefectDrawer
v-model:visible="showLinkDrawer"
:case-id="props.caseId"
:drawer-loading="drawerLoading"
@save="saveHandler"
/>
<AddDefectDrawer
v-model:visible="showDrawer"
:case-id="props.caseId"
::extra-params="{
testPlanCaseId: route.query.testPlanCaseId,
caseId: props.caseId,
testPlanId:props.testPlanId,
}"
@success="initData()"
/>
</div> </div>
</template> </template>
@ -92,12 +76,10 @@
import MsButton from '@/components/pure/ms-button/index.vue'; import MsButton from '@/components/pure/ms-button/index.vue';
import type { MsTableColumn } from '@/components/pure/ms-table/type'; import type { MsTableColumn } from '@/components/pure/ms-table/type';
import AddDefectDrawer from '@/views/case-management/caseManagementFeature/components/tabContent/tabBug/addDefectDrawer.vue';
import BugList from '@/views/case-management/caseManagementFeature/components/tabContent/tabBug/bugList.vue'; import BugList from '@/views/case-management/caseManagementFeature/components/tabContent/tabBug/bugList.vue';
import LinkDefectDrawer from '@/views/case-management/caseManagementFeature/components/tabContent/tabBug/linkDefectDrawer.vue';
import { getBugList, getCustomOptionHeader } from '@/api/modules/bug-management'; import { getBugList, getCustomOptionHeader } from '@/api/modules/bug-management';
import { associateBugToPlan, associatedBugPage, testPlanCancelBug } from '@/api/modules/test-plan/testPlan'; import { associatedBugPage, testPlanCancelBug } from '@/api/modules/test-plan/testPlan';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store'; import { useAppStore } from '@/store';
import { hasAnyPermission } from '@/utils/permission'; import { hasAnyPermission } from '@/utils/permission';
@ -116,6 +98,12 @@
const keyword = ref<string>(''); const keyword = ref<string>('');
const emit = defineEmits<{
(e: 'link'): void;
(e: 'new'): void;
(e: 'save', params: TableQueryParams): void;
}>();
const columns = ref<MsTableColumn>([ const columns = ref<MsTableColumn>([
{ {
title: 'caseManagement.featureCase.tableColumnID', title: 'caseManagement.featureCase.tableColumnID',
@ -188,26 +176,16 @@
if (!hasAnyPermission(['FUNCTIONAL_CASE:READ', 'FUNCTIONAL_CASE:READ+UPDATE', 'FUNCTIONAL_CASE:READ+DELETE'])) { if (!hasAnyPermission(['FUNCTIONAL_CASE:READ', 'FUNCTIONAL_CASE:READ+UPDATE', 'FUNCTIONAL_CASE:READ+DELETE'])) {
return; return;
} }
bugTableListRef.value?.searchData(); bugTableListRef.value?.searchData(keyword.value);
}
const showLinkDrawer = ref<boolean>(false);
function linkDefect() {
showLinkDrawer.value = true;
}
const showDrawer = ref<boolean>(false);
function createDefect() {
showDrawer.value = true;
} }
function handleSelect(value: string | number | Record<string, any> | undefined) { function handleSelect(value: string | number | Record<string, any> | undefined) {
switch (value) { switch (value) {
case 'associated': case 'associated':
linkDefect(); emit('link');
break; break;
default: default:
createDefect(); emit('new');
break; break;
} }
} }
@ -232,6 +210,7 @@
} }
const cancelLoading = ref<boolean>(false); const cancelLoading = ref<boolean>(false);
// //
async function cancelLink(id: string) { async function cancelLink(id: string) {
cancelLoading.value = true; cancelLoading.value = true;
@ -261,25 +240,10 @@
} }
} }
const route = useRoute(); const route = useRoute();
const drawerLoading = ref<boolean>(false);
// function resetHandler() {
async function saveHandler(params: TableQueryParams) { keyword.value = '';
try { initData();
drawerLoading.value = true;
await associateBugToPlan({
...params,
caseId: props.caseId,
testPlanId: props.testPlanId,
testPlanCaseId: route.query.testPlanCaseId as string,
});
Message.success(t('caseManagement.featureCase.associatedSuccess'));
initData();
showLinkDrawer.value = false;
} catch (error) {
console.log(error);
} finally {
drawerLoading.value = false;
}
} }
watch( watch(
@ -296,6 +260,10 @@
initData(); initData();
initBugList(); initBugList();
}); });
defineExpose({
initData,
});
</script> </script>
<style scoped></style> <style scoped></style>

View File

@ -1,84 +1,107 @@
<template> <template>
<!-- TODO: 待联调 --> <a-spin :loading="loading" class="w-full">
<div class="review-history-list"> <div class="execute-history-list">
<div v-for="item of executeHistoryList" :key="item.id" class="review-history-list-item"> <div v-for="item of executeHistoryList" :key="item.status" class="execute-history-list-item">
<div class="flex items-center"> <div class="flex items-center">
<MsAvatar :avatar="item.userLogo" /> <MsAvatar :avatar="item.userLogo" />
<div class="ml-[8px] flex items-center"> <div class="ml-[8px] flex items-center">
<a-tooltip :content="item.userName" :mouse-enter-delay="300"> <a-tooltip :content="item.userName" :mouse-enter-delay="300">
<div class="one-line-text max-w-[300px] font-medium text-[var(--color-text-1)]">{{ item.userName }}</div> <div class="one-line-text max-w-[300px] font-medium text-[var(--color-text-1)]">{{ item.userName }}</div>
</a-tooltip> </a-tooltip>
<a-divider direction="vertical" margin="8px"></a-divider> <a-divider direction="vertical" margin="8px"></a-divider>
<div v-if="item.status === 'PASS'" class="flex items-center"> <div v-if="item.status === 'SUCCESS'" class="flex items-center">
<MsIcon type="icon-icon_succeed_filled" class="mr-[4px] text-[rgb(var(--success-6))]" /> <MsIcon type="icon-icon_succeed_filled" class="mr-[4px] text-[rgb(var(--success-6))]" />
{{ t('caseManagement.caseReview.pass') }} {{ t('common.success') }}
</div>
<div v-if="item.status === 'BLOCKED'" class="flex items-center">
<MsIcon type="icon-icon_block_filled" class="mr-[4px] text-[rgb(var(--warning-6))]" />
{{ t('common.block') }}
</div>
<div v-if="item.status === 'ERROR'" class="flex items-center">
<MsIcon type="icon-icon_close_filled" class="mr-[4px] text-[rgb(var(--danger-6))]" />
{{ t('common.fail') }}
</div>
</div> </div>
<div v-else-if="item.status === 'UN_PASS'" class="flex items-center"> </div>
<MsIcon type="icon-icon_close_filled" class="mr-[4px] text-[rgb(var(--danger-6))]" /> <div class="markdown-body" style="margin-left: 48px" v-html="item.contentText"></div>
{{ t('caseManagement.caseReview.fail') }} <div class="ml-[48px] mt-[8px] flex text-[var(--color-text-4)]">
</div> {{ dayjs(item.createTime).format('YYYY-MM-DD HH:mm:ss') }}
<div v-else-if="item.status === 'UNDER_REVIEWED'" class="flex items-center"> <div>
<MsIcon type="icon-icon_warning_filled" class="mr-[4px] text-[rgb(var(--warning-6))]" /> <a-tooltip :content="item.userName" :mouse-enter-delay="300" :disabled="!item.userName">
{{ t('caseManagement.caseReview.suggestion') }} <span v-if="item.deleted" class="one-line-text ml-[16px] max-w-[300px] break-words break-all">
</div> {{ characterLimit(item.userName) }}
<div v-else-if="item.status === 'RE_REVIEWED'" class="flex items-center"> </span>
<MsIcon type="icon-icon_resubmit_filled" class="mr-[4px] text-[rgb(var(--warning-6))]" /> </a-tooltip>
{{ t('caseManagement.caseReview.reReview') }}
</div>
<div v-if="item.status === 'PASSED'" class="flex items-center">
<MsIcon type="icon-icon_succeed_filled" class="mr-[4px] text-[rgb(var(--success-6))]" />
{{ t('caseManagement.featureCase.execute.success') }}
</div>
<div v-if="item.status === 'BLOCKED'" class="flex items-center">
<MsIcon type="icon-icon_succeed_filled" class="mr-[4px] text-[rgb(var(--success-6))]" />
{{ t('caseManagement.featureCase.execute.blocked') }}
</div>
<div v-if="item.status === 'FAILED'" class="flex items-center">
<MsIcon type="icon-icon_succeed_filled" class="mr-[4px] text-[rgb(var(--success-6))]" />
{{ t('caseManagement.featureCase.execute.failed') }}
</div> </div>
</div> </div>
</div> </div>
<div class="markdown-body" style="margin-left: 48px" v-html="item.contentText"></div> <MsEmpty v-if="executeHistoryList.length === 0" />
<div class="ml-[48px] mt-[8px] flex text-[var(--color-text-4)]">
{{ dayjs(item.createTime).format('YYYY-MM-DD HH:mm:ss') }}
<div>
<a-tooltip :content="item.reviewName" :mouse-enter-delay="300">
<span v-if="item.deleted" class="one-line-text ml-[16px] max-w-[300px] break-words break-all">
{{ characterLimit(item.reviewName) }}
</span>
<span
v-else
class="one-line-text ml-[16px] max-w-[300px] cursor-pointer break-words break-all text-[rgb(var(--primary-5))]"
>
{{ characterLimit(item.reviewName) }}
</span>
</a-tooltip>
</div>
</div>
</div> </div>
<MsEmpty v-if="executeHistoryList.length === 0" /> </a-spin>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
import { useRoute } from 'vue-router';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import MsAvatar from '@/components/pure/ms-avatar/index.vue'; import MsAvatar from '@/components/pure/ms-avatar/index.vue';
import MsEmpty from '@/components/pure/ms-empty/index.vue'; import MsEmpty from '@/components/pure/ms-empty/index.vue';
import { CommentItem, CommentParams } from '@/components/business/ms-comment/types';
import { executeHistory } from '@/api/modules/test-plan/testPlan';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { characterLimit } from '@/utils'; import { characterLimit } from '@/utils';
import type { ExecuteHistoryItem } from '@/models/testPlan/testPlan';
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps<{ const props = defineProps<{
caseId: string; caseId: string;
}>(); }>();
const executeHistoryList = ref<CommentItem[]>([]); const executeHistoryList = ref<ExecuteHistoryItem[]>([]);
const route = useRoute();
const loading = ref<boolean>(false);
async function initList() {
loading.value = true;
try {
executeHistoryList.value = await executeHistory({
caseId: props.caseId,
id: route.query.testPlanCaseId as string,
testPlanId: route.query.id as string,
});
} catch (error) {
console.log(error);
} finally {
loading.value = false;
}
}
onBeforeMount(() => {
initList();
});
watch(
() => props.caseId,
(val) => {
if (val) {
initList();
}
}
);
</script> </script>
<style scoped></style> <style scoped lang="less">
.execute-history-list {
height: calc(100vh - 240px);
@apply overflow-auto;
.ms-scroll-bar();
.execute-history-list-item {
&:not(:last-child) {
margin-bottom: 16px;
}
}
}
</style>

View File

@ -120,7 +120,33 @@
size="16" size="16"
/> />
</a-tooltip> </a-tooltip>
<!-- TODO: 缺陷 --> <MsTag type="danger" theme="light" size="medium" class="ml-4">
<MsIcon type="icon-icon_defect" class="!text-[14px] text-[rgb(var(--danger-6))]" size="16" />
<span class="ml-1 text-[rgb(var(--danger-6))]"> {{ t('testPlan.featureCase.bug') }}</span>
<span class="ml-1 text-[rgb(var(--danger-6))]">{{ bugCount }}</span>
</MsTag>
<a-dropdown @select="handleSelect">
<a-button type="outline" size="mini" class="ml-1">
<template #icon> <icon-plus class="text-[12px]" /> </template>
</a-button>
<template #content>
<a-doption value="new">{{ t('common.newCreate') }}</a-doption>
<a-doption v-if="createdBugCount > 0" value="link">{{ t('common.associated') }}</a-doption>
<a-popover v-else title="" position="left">
<a-doption :disabled="true" value="link">{{ t('common.associated') }}</a-doption>
<template #content>
<div class="flex items-center text-[14px]">
<span class="text-[var(--color-text-4)]">{{
t('testPlan.featureCase.noBugDataTooltip')
}}</span>
<MsButton type="text" @click="handleSelect('new')">
{{ t('testPlan.featureCase.noBugDataNewBug') }}
</MsButton>
</div>
</template>
</a-popover>
</template>
</a-dropdown>
</div> </div>
</div> </div>
<ExecuteSubmit <ExecuteSubmit
@ -134,19 +160,39 @@
</div> </div>
<BugList <BugList
v-if="activeTab === 'defectList'" v-if="activeTab === 'defectList'"
:case-id="caseDetail.id" ref="bugRef"
:case-id="activeCaseId"
:test-plan-id="route.query.id as string" :test-plan-id="route.query.id as string"
@link="linkDefect"
@new="addBug"
/> />
<ExecutionHistory v-if="activeTab === 'executionHistory'" :case-id="caseDetail.id" /> <ExecutionHistory v-if="activeTab === 'executionHistory'" :case-id="activeCaseId" />
</div> </div>
</a-spin> </a-spin>
</div> </div>
</MsCard> </MsCard>
<EditCaseDetailDrawer v-model:visible="editCaseVisible" :case-id="activeCaseId" @load-case="loadCase" /> <EditCaseDetailDrawer v-model:visible="editCaseVisible" :case-id="activeCaseId" @load-case="loadCase" />
<LinkDefectDrawer
v-model:visible="showLinkDrawer"
:case-id="activeCaseId"
:drawer-loading="drawerLoading"
@save="associateSuccessHandler"
/>
<AddDefectDrawer
v-model:visible="showDrawer"
:case-id="activeCaseId"
::extra-params="{
testPlanCaseId: route.query.testPlanCaseId,
caseId: activeCaseId,
testPlanId:route.query.id as string,
}"
@success="addSuccess"
/>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { Message } from '@arco-design/web-vue';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import MsCard from '@/components/pure/ms-card/index.vue'; import MsCard from '@/components/pure/ms-card/index.vue';
@ -154,19 +200,31 @@
import MsEmpty from '@/components/pure/ms-empty/index.vue'; import MsEmpty from '@/components/pure/ms-empty/index.vue';
import MsPagination from '@/components/pure/ms-pagination/index'; import MsPagination from '@/components/pure/ms-pagination/index';
import MsTab from '@/components/pure/ms-tab/index.vue'; import MsTab from '@/components/pure/ms-tab/index.vue';
import MsTag from '@/components/pure/ms-tag/ms-tag.vue';
import ExecuteResult from '@/components/business/ms-case-associate/executeResult.vue'; import ExecuteResult from '@/components/business/ms-case-associate/executeResult.vue';
import MsStatusTag from '@/components/business/ms-status-tag/index.vue'; import MsStatusTag from '@/components/business/ms-status-tag/index.vue';
import BugList from './bug/index.vue'; import BugList from './bug/index.vue';
import ExecuteSubmit from './executeSubmit.vue'; import ExecuteSubmit from './executeSubmit.vue';
import AddDefectDrawer from '@/views/case-management/caseManagementFeature/components/tabContent/tabBug/addDefectDrawer.vue';
import LinkDefectDrawer from '@/views/case-management/caseManagementFeature/components/tabContent/tabBug/linkDefectDrawer.vue';
import CaseTabDetail from '@/views/case-management/caseManagementFeature/components/tabContent/tabDetail.vue'; import CaseTabDetail from '@/views/case-management/caseManagementFeature/components/tabContent/tabDetail.vue';
import EditCaseDetailDrawer from '@/views/case-management/caseReview/components/editCaseDetailDrawer.vue'; import EditCaseDetailDrawer from '@/views/case-management/caseReview/components/editCaseDetailDrawer.vue';
import ExecutionHistory from '@/views/test-plan/testPlan/detail/featureCase/detail/executionHistory/index.vue'; import ExecutionHistory from '@/views/test-plan/testPlan/detail/featureCase/detail/executionHistory/index.vue';
import { getCaseDetail, getPlanDetailFeatureCaseList, getTestPlanDetail } from '@/api/modules/test-plan/testPlan'; import { getBugList } from '@/api/modules/bug-management';
import {
associateBugToPlan,
associatedBugPage,
getCaseDetail,
getPlanDetailFeatureCaseList,
getTestPlanDetail,
} from '@/api/modules/test-plan/testPlan';
import { testPlanDefaultDetail } from '@/config/testPlan'; import { testPlanDefaultDetail } from '@/config/testPlan';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
import { hasAnyPermission } from '@/utils/permission';
import type { TableQueryParams } from '@/models/common';
import type { PlanDetailFeatureCaseItem, TestPlanDetail } from '@/models/testPlan/testPlan'; import type { PlanDetailFeatureCaseItem, TestPlanDetail } from '@/models/testPlan/testPlan';
import { LastExecuteResults } from '@/enums/caseEnum'; import { LastExecuteResults } from '@/enums/caseEnum';
import { CaseManagementRouteEnum } from '@/enums/routeEnum'; import { CaseManagementRouteEnum } from '@/enums/routeEnum';
@ -338,7 +396,6 @@
() => activeId.value, () => activeId.value,
() => { () => {
loadCaseDetail(); loadCaseDetail();
// TODO
} }
); );
@ -389,6 +446,95 @@
} }
} }
const bugCount = ref<number>(0);
const showLinkDrawer = ref<boolean>(false);
const drawerLoading = ref<boolean>(false);
const showDrawer = ref<boolean>(false);
function addBug() {
showDrawer.value = true;
}
function linkDefect() {
showLinkDrawer.value = true;
}
function handleSelect(value: string | number | Record<string, any> | undefined) {
switch (value) {
case 'new':
addBug();
break;
default:
linkDefect();
break;
}
}
const bugRef = ref();
async function getBugTotal() {
try {
const params = {
testPlanCaseId: route.query.testPlanCaseId,
caseId: activeCaseId.value,
projectId: appStore.currentProjectId,
current: 1,
pageSize: 10,
};
const res = await associatedBugPage(params);
bugCount.value = res.total;
} catch (error) {
console.log(error);
}
}
function addSuccess() {
if (activeTab.value === 'defectList') {
bugRef.value?.initData();
} else {
getBugTotal();
}
}
async function associateSuccessHandler(params: TableQueryParams) {
try {
drawerLoading.value = true;
await associateBugToPlan({
...params,
caseId: activeCaseId.value,
testPlanId: route.query.id as string,
testPlanCaseId: route.query.testPlanCaseId as string,
});
Message.success(t('caseManagement.featureCase.associatedSuccess'));
showLinkDrawer.value = false;
addSuccess();
} catch (error) {
console.log(error);
} finally {
drawerLoading.value = false;
}
}
const createdBugCount = ref<number>(0);
async function initBugList() {
if (!hasAnyPermission(['PROJECT_BUG:READ'])) {
return;
}
const res = await getBugList({
current: 1,
pageSize: 10,
sort: {},
filter: {},
keyword: '',
combine: {},
searchMode: 'AND',
projectId: appStore.currentProjectId,
});
createdBugCount.value = res.total;
}
onBeforeMount(async () => { onBeforeMount(async () => {
const lastPageParams = window.history.state.params ? JSON.parse(window.history.state.params) : null; // const lastPageParams = window.history.state.params ? JSON.parse(window.history.state.params) : null; //
if (lastPageParams) { if (lastPageParams) {
@ -404,7 +550,11 @@
moduleIds, moduleIds,
}; };
} }
if (activeTab.value === 'detail') {
getBugTotal();
}
getPlanDetail(); getPlanDetail();
initBugList();
await loadCase(); await loadCase();
}); });
</script> </script>

View File

@ -76,4 +76,12 @@
function initModuleTree(tree: ModuleTreeNode[]) { function initModuleTree(tree: ModuleTreeNode[]) {
moduleTree.value = unref(tree); moduleTree.value = unref(tree);
} }
function getCaseTableList() {
caseTableRef.value?.loadCaseList();
}
defineExpose({
getCaseTableList,
});
</script> </script>

View File

@ -101,11 +101,21 @@
</MsCard> </MsCard>
<!-- special-height的174: 上面卡片高度158 + mt的16 --> <!-- special-height的174: 上面卡片高度158 + mt的16 -->
<MsCard class="mt-[16px]" :special-height="174" simple has-breadcrumb no-content-padding> <MsCard class="mt-[16px]" :special-height="174" simple has-breadcrumb no-content-padding>
<FeatureCase v-if="activeTab === 'featureCase'" :repeat-case="detail.repeatCase" @refresh="getStatistics" /> <FeatureCase
v-if="activeTab === 'featureCase'"
ref="featureCaseRef"
:repeat-case="detail.repeatCase"
@refresh="getStatistics"
/>
<!-- TODO 先不上 --> <!-- TODO 先不上 -->
<!-- <BugManagement v-if="activeTab === 'defectList'" :plan-id="detail.id" /> --> <!-- <BugManagement v-if="activeTab === 'defectList'" :plan-id="detail.id" /> -->
</MsCard> </MsCard>
<AssociateDrawer v-model:visible="caseAssociateVisible" :associated-ids="hasSelectedIds" @success="success" /> <AssociateDrawer
v-model:visible="caseAssociateVisible"
:associated-ids="detail.repeatCase ? hasSelectedIds : []"
:save-api="associationCaseToPlan"
@success="handleSuccess"
/>
<CreateAndEditPlanDrawer <CreateAndEditPlanDrawer
v-model:visible="showPlanDrawer" v-model:visible="showPlanDrawer"
:plan-id="planId" :plan-id="planId"
@ -117,7 +127,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref } from 'vue'; import { computed, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es'; import { cloneDeep } from 'lodash-es';
@ -153,12 +163,7 @@
import { characterLimit } from '@/utils'; import { characterLimit } from '@/utils';
import { ModuleTreeNode } from '@/models/common'; import { ModuleTreeNode } from '@/models/common';
import type { import type { PassRateCountDetail, TestPlanDetail, TestPlanItem } from '@/models/testPlan/testPlan';
AssociateCaseRequest,
PassRateCountDetail,
TestPlanDetail,
TestPlanItem,
} from '@/models/testPlan/testPlan';
const userStore = useUserStore(); const userStore = useUserStore();
const appStore = useAppStore(); const appStore = useAppStore();
@ -353,6 +358,7 @@
function successHandler() { function successHandler() {
initDetail(); initDetail();
} }
const testPlanTree = ref<ModuleTreeNode[]>([]); const testPlanTree = ref<ModuleTreeNode[]>([]);
async function initPlanTree() { async function initPlanTree() {
try { try {
@ -362,19 +368,10 @@
} }
} }
// const featureCaseRef = ref<InstanceType<typeof FeatureCase>>();
async function success(params: AssociateCaseRequest) { function handleSuccess() {
try { initDetail();
await associationCaseToPlan({ featureCaseRef.value?.getCaseTableList();
functionalSelectIds: params.selectIds,
testPlanId: planId.value,
});
Message.success(t('ms.case.associate.associateSuccess'));
caseAssociateVisible.value = false;
initDetail();
} catch (error) {
console.log(error);
}
} }
onBeforeMount(() => { onBeforeMount(() => {

View File

@ -32,7 +32,7 @@
isExpandAll ? t('testPlan.testPlanIndex.collapseAll') : t('testPlan.testPlanIndex.expandAll') isExpandAll ? t('testPlan.testPlanIndex.collapseAll') : t('testPlan.testPlanIndex.expandAll')
" "
> >
<MsButton type="icon" status="secondary" class="!mr-0 p-[4px]" @click="expandHandler"> <MsButton type="icon" status="secondary" class="!mr-0 p-[4px]" position="top" @click="expandHandler">
<MsIcon :type="isExpandAll ? 'icon-icon_folder_collapse1' : 'icon-icon_folder_expansion1'" /> <MsIcon :type="isExpandAll ? 'icon-icon_folder_collapse1' : 'icon-icon_folder_expansion1'" />
</MsButton> </MsButton>
</a-tooltip> </a-tooltip>

View File

@ -19,6 +19,7 @@ export default {
'testPlan.testPlanIndex.passRate': 'Pass Rate', 'testPlan.testPlanIndex.passRate': 'Pass Rate',
'testPlan.testPlanIndex.useCount': 'Use cases', 'testPlan.testPlanIndex.useCount': 'Use cases',
'testPlan.testPlanIndex.bugCount': 'bug count', 'testPlan.testPlanIndex.bugCount': 'bug count',
'testPlan.featureCase.bug': 'bug',
'testPlan.testPlanIndex.belongModule': 'belong module', 'testPlan.testPlanIndex.belongModule': 'belong module',
'testPlan.testPlanIndex.createTime': 'create time', 'testPlan.testPlanIndex.createTime': 'create time',
'testPlan.testPlanIndex.operation': 'operation', 'testPlan.testPlanIndex.operation': 'operation',

View File

@ -88,6 +88,7 @@ export default {
'testPlan.bugManagement.defectState': '缺陷状态', 'testPlan.bugManagement.defectState': '缺陷状态',
'testPlan.bugManagement.caseClassification': '用例分类', 'testPlan.bugManagement.caseClassification': '用例分类',
'testPlan.featureCase.bugCount': '缺陷数', 'testPlan.featureCase.bugCount': '缺陷数',
'testPlan.featureCase.bug': '缺陷',
'testPlan.featureCase.executor': '执行人', 'testPlan.featureCase.executor': '执行人',
'testPlan.featureCase.changeExecutor': '修改执行人', 'testPlan.featureCase.changeExecutor': '修改执行人',
'testPlan.featureCase.batchChangeExecutor': '批量修改执行人', 'testPlan.featureCase.batchChangeExecutor': '批量修改执行人',