feat(测试计划): 关联功能用例

This commit is contained in:
teukkk 2024-05-09 11:14:48 +08:00 committed by 刘瑞斌
parent 26ac5c4636
commit 91615d9e45
9 changed files with 202 additions and 17 deletions

View File

@ -7,11 +7,13 @@ import {
deletePlanUrl, deletePlanUrl,
DeleteTestPlanModuleUrl, DeleteTestPlanModuleUrl,
getStatisticalCountUrl, getStatisticalCountUrl,
GetTestPlanDetailUrl,
GetTestPlanListUrl, GetTestPlanListUrl,
GetTestPlanModuleCountUrl, GetTestPlanModuleCountUrl,
GetTestPlanModuleUrl, GetTestPlanModuleUrl,
MoveTestPlanModuleUrl, MoveTestPlanModuleUrl,
updateTestPlanModuleUrl, updateTestPlanModuleUrl,
UpdateTestPlanUrl,
} from '@/api/requrls/test-plan/testPlan'; } from '@/api/requrls/test-plan/testPlan';
import type { CreateOrUpdateModule, UpdateModule } from '@/models/caseManagement/featureCase'; import type { CreateOrUpdateModule, UpdateModule } from '@/models/caseManagement/featureCase';
@ -58,6 +60,16 @@ export function getTestPlanList(data: TableQueryParams) {
export function addTestPlan(data: AddTestPlanParams) { export function addTestPlan(data: AddTestPlanParams) {
return MSR.post({ url: AddTestPlanUrl, data }); return MSR.post({ url: AddTestPlanUrl, data });
} }
// 获取测试计划详情
export function getTestPlanDetail(id: string) {
return MSR.get<AddTestPlanParams>({ url: `${GetTestPlanDetailUrl}/${id}` });
}
// 更新测试计划
export function updateTestPlan(data: AddTestPlanParams) {
return MSR.post({ url: UpdateTestPlanUrl, data });
}
// 批量删除测试计划 // 批量删除测试计划
export function batchDeletePlan(data: TableQueryParams) { export function batchDeletePlan(data: TableQueryParams) {
return MSR.post({ url: batchDeletePlanUrl, data }); return MSR.post({ url: batchDeletePlanUrl, data });

View File

@ -14,6 +14,10 @@ export const GetTestPlanModuleCountUrl = '/test-plan/module/count';
export const GetTestPlanListUrl = '/test-plan/page'; export const GetTestPlanListUrl = '/test-plan/page';
// 创建测试计划 // 创建测试计划
export const AddTestPlanUrl = '/test-plan/add'; export const AddTestPlanUrl = '/test-plan/add';
// 获取测试计划详情
export const GetTestPlanDetailUrl = '/test-plan';
// 更新测试计划
export const UpdateTestPlanUrl = '/test-plan/update';
// 批量删除测试计划 // 批量删除测试计划
export const batchDeletePlanUrl = '/test-plan/batch-delete'; export const batchDeletePlanUrl = '/test-plan/batch-delete';
// 删除测试计划 // 删除测试计划

View File

@ -33,6 +33,7 @@ export interface AssociateCaseRequest extends BatchApiParams {
apiSelectIds?: string[]; apiSelectIds?: string[];
apiCaseSelectIds?: string[]; apiCaseSelectIds?: string[];
apiScenarioSelectIds?: string[]; apiScenarioSelectIds?: string[];
totalCount?: number;
} }
export interface AddTestPlanParams { export interface AddTestPlanParams {

View File

@ -0,0 +1,58 @@
<template>
<MsCaseAssociate
v-model:visible="innerVisible"
v-model:currentSelectCase="currentSelectCase"
:get-modules-func="getCaseModuleTree"
:get-table-func="getCaseList"
:confirm-loading="confirmLoading"
:associated-ids="[]"
:project-id="currentProjectId"
:type="RequestModuleEnum.CASE_MANAGEMENT"
hide-project-select
is-hidden-case-level
:has-not-associated-ids="props.hasNotAssociatedIds"
@save="saveHandler"
>
</MsCaseAssociate>
</template>
<script setup lang="ts">
import MsCaseAssociate from '@/components/business/ms-case-associate/index.vue';
import { RequestModuleEnum } from '@/components/business/ms-case-associate/utils';
import { getCaseList, getCaseModuleTree } from '@/api/modules/case-management/featureCase';
import useAppStore from '@/store/modules/app';
import type { AssociateCaseRequest } from '@/models/testPlan/testPlan';
import { CaseLinkEnum } from '@/enums/caseEnum';
const props = defineProps<{
hasNotAssociatedIds?: string[];
}>();
const innerVisible = defineModel<boolean>('visible', {
required: true,
});
const emit = defineEmits<{
(e: 'success', val: AssociateCaseRequest): void;
}>();
const appStore = useAppStore();
const currentSelectCase = ref<keyof typeof CaseLinkEnum>('FUNCTIONAL');
const currentProjectId = ref(appStore.currentProjectId);
const confirmLoading = ref<boolean>(false);
function saveHandler(params: AssociateCaseRequest) {
try {
confirmLoading.value = true;
emit('success', { ...params });
innerVisible.value = false;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
confirmLoading.value = false;
}
}
</script>

View File

@ -198,7 +198,9 @@
<MsButton class="!mx-0">{{ t('testPlan.testPlanIndex.execution') }}</MsButton> <MsButton class="!mx-0">{{ t('testPlan.testPlanIndex.execution') }}</MsButton>
<a-divider direction="vertical" :margin="8"></a-divider> <a-divider direction="vertical" :margin="8"></a-divider>
<MsButton v-permission="['PROJECT_TEST_PLAN:READ+UPDATE']" class="!mx-0">{{ t('common.edit') }}</MsButton> <MsButton v-permission="['PROJECT_TEST_PLAN:READ+UPDATE']" class="!mx-0" @click="emit('edit', record.id)">{{
t('common.edit')
}}</MsButton>
<a-divider direction="vertical" :margin="8"></a-divider> <a-divider direction="vertical" :margin="8"></a-divider>
<MsTableMoreAction :list="moreActions" @select="handleMoreActionSelect($event, record)" /> <MsTableMoreAction :list="moreActions" @select="handleMoreActionSelect($event, record)" />
@ -301,6 +303,7 @@
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'init', params: any): void; (e: 'init', params: any): void;
(e: 'edit', id: string): void;
}>(); }>();
const columns: MsTableColumn = [ const columns: MsTableColumn = [
@ -592,10 +595,14 @@
}; };
} }
async function fetchData() { async function loadPlanList() {
resetSelector();
setLoadListParams(await initTableParams()); setLoadListParams(await initTableParams());
loadList(); loadList();
}
async function fetchData() {
resetSelector();
await loadPlanList();
const tableParams = await initTableParams(); const tableParams = await initTableParams();
emit('init', { emit('init', {
...tableParams, ...tableParams,
@ -905,6 +912,10 @@
fetchData(); fetchData();
}); });
defineExpose({
loadPlanList,
});
await tableStore.initColumn(TableKeyEnum.TEST_PLAN_ALL_TABLE, columns, 'drawer'); await tableStore.initColumn(TableKeyEnum.TEST_PLAN_ALL_TABLE, columns, 'drawer');
</script> </script>

View File

@ -1,12 +1,12 @@
<template> <template>
<MsDrawer <MsDrawer
v-model:visible="innerVisible" v-model:visible="innerVisible"
:title="form.id ? t('case.updateCase') : t('testPlan.testPlanIndex.createTestPlan')" :title="props.planId?.length ? t('case.updateCase') : t('testPlan.testPlanIndex.createTestPlan')"
:width="800" :width="800"
unmount-on-close unmount-on-close
:ok-text="form.id ? 'common.update' : 'common.create'" :ok-text="props.planId?.length ? 'common.update' : 'common.create'"
:save-continue-text="t('case.saveContinueText')" :save-continue-text="t('case.saveContinueText')"
:show-continue="!form.id" :show-continue="!props.planId?.length"
:ok-loading="drawerLoading" :ok-loading="drawerLoading"
@confirm="handleDrawerConfirm(false)" @confirm="handleDrawerConfirm(false)"
@continue="handleDrawerConfirm(true)" @continue="handleDrawerConfirm(true)"
@ -68,6 +68,36 @@
<a-form-item field="tags" :label="t('common.tag')" class="w-[436px]"> <a-form-item field="tags" :label="t('common.tag')" class="w-[436px]">
<MsTagsInput v-model:model-value="form.tags" :max-tag-count="10" :max-length="50" /> <MsTagsInput v-model:model-value="form.tags" :max-tag-count="10" :max-length="50" />
</a-form-item> </a-form-item>
<a-form-item v-if="!props.planId?.length">
<template #label>
<div class="flex items-center">
{{ t('testPlan.planForm.pickCases') }}
<a-divider margin="4px" direction="vertical" />
<MsButton
type="text"
:disabled="form.baseAssociateCaseRequest.selectIds.length === 0"
@click="clearSelectedCases"
>
{{ t('caseManagement.caseReview.clearSelectedCases') }}
</MsButton>
</div>
</template>
<div class="flex w-[436px] items-center rounded bg-[var(--color-text-n9)] p-[12px]">
<div class="text-[var(--color-text-2)]">
{{
t('caseManagement.caseReview.selectedCases', {
count: form.baseAssociateCaseRequest.selectAll
? form.baseAssociateCaseRequest.totalCount
: form.baseAssociateCaseRequest.selectIds.length,
})
}}
</div>
<a-divider margin="8px" direction="vertical" />
<MsButton type="text" class="font-medium" @click="caseAssociateVisible = true">
{{ t('ms.case.associate.title') }}
</MsButton>
</div>
</a-form-item>
<MsMoreSettingCollapse> <MsMoreSettingCollapse>
<template #content> <template #content>
<div v-for="item in switchList" :key="item.key" class="mb-[24px] flex items-center gap-[8px]"> <div v-for="item in switchList" :key="item.key" class="mb-[24px] flex items-center gap-[8px]">
@ -95,35 +125,48 @@
</MsMoreSettingCollapse> </MsMoreSettingCollapse>
</a-form> </a-form>
</MsDrawer> </MsDrawer>
<AssociateDrawer
v-model:visible="caseAssociateVisible"
:has-not-associated-ids="form.baseAssociateCaseRequest.selectIds"
@success="writeAssociateCases"
/>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
import { FormInstance, Message, TreeNodeData } from '@arco-design/web-vue'; import { FormInstance, Message, TreeNodeData, ValidatedError } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es'; import { cloneDeep } from 'lodash-es';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue'; import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsMoreSettingCollapse from '@/components/pure/ms-more-setting-collapse/index.vue'; import MsMoreSettingCollapse from '@/components/pure/ms-more-setting-collapse/index.vue';
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue'; import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
import AssociateDrawer from './components/associateDrawer.vue';
import { addTestPlan } from '@/api/modules/test-plan/testPlan'; import { addTestPlan, getTestPlanDetail, updateTestPlan } 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 { ModuleTreeNode } from '@/models/common'; import { ModuleTreeNode } from '@/models/common';
import type { AddTestPlanParams, SwitchListModel } from '@/models/testPlan/testPlan'; import type { AddTestPlanParams, AssociateCaseRequest, SwitchListModel } from '@/models/testPlan/testPlan';
import { testPlanTypeEnum } from '@/enums/testPlanEnum'; import { testPlanTypeEnum } from '@/enums/testPlanEnum';
const { t } = useI18n();
const appStore = useAppStore();
const props = defineProps<{ const props = defineProps<{
planId?: string;
moduleTree?: ModuleTreeNode[]; moduleTree?: ModuleTreeNode[];
}>(); }>();
const innerVisible = defineModel<boolean>('visible', { const innerVisible = defineModel<boolean>('visible', {
required: true, required: true,
}); });
const emit = defineEmits<{
(e: 'close'): void;
(e: 'loadPlanList'): void;
}>();
const { t } = useI18n();
const appStore = useAppStore();
const drawerLoading = ref(false); const drawerLoading = ref(false);
const formRef = ref<FormInstance>(); const formRef = ref<FormInstance>();
const initForm: AddTestPlanParams = { const initForm: AddTestPlanParams = {
@ -155,29 +198,40 @@
}, },
]; ];
const caseAssociateVisible = ref(false);
function clearSelectedCases() {
form.value.baseAssociateCaseRequest = cloneDeep(initForm.baseAssociateCaseRequest);
}
function writeAssociateCases(param: AssociateCaseRequest) {
form.value.baseAssociateCaseRequest = { ...param };
}
function handleCancel() { function handleCancel() {
innerVisible.value = false; innerVisible.value = false;
formRef.value?.resetFields(); formRef.value?.resetFields();
form.value = cloneDeep(initForm); form.value = cloneDeep(initForm);
emit('close');
} }
function handleDrawerConfirm(isContinue: boolean) { function handleDrawerConfirm(isContinue: boolean) {
formRef.value?.validate(async (errors) => { formRef.value?.validate(async (errors: undefined | Record<string, ValidatedError>) => {
if (!errors) { if (!errors) {
drawerLoading.value = true; drawerLoading.value = true;
try { try {
// TODO
const params: AddTestPlanParams = { const params: AddTestPlanParams = {
...cloneDeep(form.value), ...cloneDeep(form.value),
plannedStartTime: form.value.cycle ? form.value.cycle[0] : undefined, plannedStartTime: form.value.cycle ? form.value.cycle[0] : undefined,
plannedEndTime: form.value.cycle ? form.value.cycle[1] : undefined, plannedEndTime: form.value.cycle ? form.value.cycle[1] : undefined,
projectId: appStore.currentProjectId, projectId: appStore.currentProjectId,
}; };
if (!form.value?.id) { if (!props.planId?.length) {
await addTestPlan(params); await addTestPlan(params);
Message.success(t('common.createSuccess')); Message.success(t('common.createSuccess'));
} else {
await updateTestPlan(params);
Message.success(t('common.updateSuccess'));
} }
// TODO emit('loadPlanList');
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(error); console.log(error);
@ -191,4 +245,26 @@
} }
}); });
} }
async function getDetail() {
try {
if (props.planId?.length) {
const result = await getTestPlanDetail(props.planId);
form.value = cloneDeep(result);
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
watch(
() => innerVisible.value,
(val) => {
if (val) {
form.value = cloneDeep(initForm);
getDetail();
}
}
);
</script> </script>

View File

@ -78,17 +78,25 @@
<template #second> <template #second>
<div class="p-[16px]"> <div class="p-[16px]">
<PlanTable <PlanTable
ref="planTableRef"
:active-folder="activeFolder" :active-folder="activeFolder"
:offspring-ids="offspringIds" :offspring-ids="offspringIds"
:active-folder-type="activeCaseType" :active-folder-type="activeCaseType"
:modules-count="modulesCount" :modules-count="modulesCount"
:node-name="nodeName" :node-name="nodeName"
@init="initModulesCount" @init="initModulesCount"
@edit="handleEdit"
/> />
</div> </div>
</template> </template>
</MsSplitBox> </MsSplitBox>
<CreateAndEditPlanDrawer v-model:visible="showPlanDrawer" :module-tree="folderTree" /> <CreateAndEditPlanDrawer
v-model:visible="showPlanDrawer"
:plan-id="planId"
:module-tree="folderTree"
@close="resetPlanId"
@load-plan-list="loadPlanList"
/>
</MsCard> </MsCard>
</template> </template>
@ -220,6 +228,19 @@
break; break;
} }
} }
const planTableRef = ref<InstanceType<typeof PlanTable>>();
const planId = ref('');
function handleEdit(id: string) {
planId.value = id;
showPlanDrawer.value = true;
}
function resetPlanId() {
planId.value = '';
}
function loadPlanList() {
planTableRef.value?.loadPlanList();
}
</script> </script>
<style scoped lang="less"> <style scoped lang="less">

View File

@ -79,4 +79,5 @@ export default {
'testPlan.planForm.passThreshold': 'Pass threshold', 'testPlan.planForm.passThreshold': 'Pass threshold',
'testPlan.planForm.repeatCaseTip1': 'Enable: Repeatedly associate the same case', 'testPlan.planForm.repeatCaseTip1': 'Enable: Repeatedly associate the same case',
'testPlan.planForm.repeatCaseTip2': 'Close: Cannot be associated with the same case repeatedly', 'testPlan.planForm.repeatCaseTip2': 'Close: Cannot be associated with the same case repeatedly',
'testPlan.planForm.pickCases': 'Select cases',
}; };

View File

@ -77,4 +77,5 @@ export default {
'testPlan.planForm.passThreshold': '通过阀值', 'testPlan.planForm.passThreshold': '通过阀值',
'testPlan.planForm.repeatCaseTip1': '开启:可重复关联同一个用例', 'testPlan.planForm.repeatCaseTip1': '开启:可重复关联同一个用例',
'testPlan.planForm.repeatCaseTip2': '关闭:不可重复关联同一用例', 'testPlan.planForm.repeatCaseTip2': '关闭:不可重复关联同一用例',
'testPlan.planForm.pickCases': '选择用例',
}; };