feat(测试计划): 测试计划关联用例选择交互&关联用例联调_中

This commit is contained in:
xinxin.wu 2024-07-10 21:45:33 +08:00 committed by 刘瑞斌
parent ecb6200759
commit 59da4f4ebc
18 changed files with 912 additions and 144 deletions

View File

@ -1,8 +1,6 @@
<template> <template>
<a-config-provider :locale="locale"> <a-config-provider :locale="locale">
<a-spin :loading="loading">
<router-view /> <router-view />
</a-spin>
<!-- <global-setting /> --> <!-- <global-setting /> -->
</a-config-provider> </a-config-provider>
</template> </template>
@ -46,8 +44,6 @@
} }
}); });
const loading = ref(false);
// //
watchStyle(appStore.pageConfig.style, appStore.pageConfig); watchStyle(appStore.pageConfig.style, appStore.pageConfig);
watchTheme(appStore.pageConfig.theme, appStore.pageConfig); watchTheme(appStore.pageConfig.theme, appStore.pageConfig);
@ -94,24 +90,26 @@
state.value = getQueryVariable('state') || ''; state.value = getQueryVariable('state') || '';
if (state.value.split('#')[0] === 'fit2cloud-lark-qr') { if (state.value.split('#')[0] === 'fit2cloud-lark-qr') {
try { try {
loading.value = true; appStore.showLoading();
const larkCallback = await getLarkCallback(code || ''); const larkCallback = await getLarkCallback(code || '');
userStore.qrCodeLogin(larkCallback); userStore.qrCodeLogin(larkCallback);
setLoginExpires(); setLoginExpires();
loading.value = false;
} catch (err) { } catch (err) {
console.log(err); console.log(err);
} finally {
appStore.hideLoading();
} }
} }
if (state.value.split('#')[0] === 'fit2cloud-lark-suite-qr') { if (state.value.split('#')[0] === 'fit2cloud-lark-suite-qr') {
try { try {
loading.value = true; appStore.showLoading();
const larkCallback = await getLarkSuiteCallback(code || ''); const larkCallback = await getLarkSuiteCallback(code || '');
userStore.qrCodeLogin(larkCallback); userStore.qrCodeLogin(larkCallback);
setLoginExpires(); setLoginExpires();
loading.value = false;
} catch (err) { } catch (err) {
console.log(err); console.log(err);
} finally {
appStore.hideLoading();
} }
} }
await userStore.checkIsLogin(); await userStore.checkIsLogin();

View File

@ -8,6 +8,9 @@
moreAction: [], moreAction: [],
}" }"
v-on="propsEvent" v-on="propsEvent"
@row-select-change="rowSelectChange"
@select-all-change="selectAllChange"
@clear-selector="clearSelector"
> >
<template #num="{ record }"> <template #num="{ record }">
<MsButton type="text" @click="toDetail(record)">{{ record.num }}</MsButton> <MsButton type="text" @click="toDetail(record)">{{ record.num }}</MsButton>
@ -41,6 +44,9 @@
<template #[FilterSlotNameEnum.API_TEST_CASE_API_LAST_EXECUTE_STATUS]="{ filterContent }"> <template #[FilterSlotNameEnum.API_TEST_CASE_API_LAST_EXECUTE_STATUS]="{ filterContent }">
<ExecutionStatus :module-type="ReportEnum.API_REPORT" :status="filterContent.value" /> <ExecutionStatus :module-type="ReportEnum.API_REPORT" :status="filterContent.value" />
</template> </template>
<template #count>
<slot></slot>
</template>
</MsBaseTable> </MsBaseTable>
</template> </template>
@ -54,6 +60,7 @@
import useTable from '@/components/pure/ms-table/useTable'; import useTable from '@/components/pure/ms-table/useTable';
import CaseLevel from '@/components/business/ms-case-associate/caseLevel.vue'; import CaseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
import ExecuteResult from '@/components/business/ms-case-associate/executeResult.vue'; import ExecuteResult from '@/components/business/ms-case-associate/executeResult.vue';
import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
import ExecutionStatus from '@/views/api-test/report/component/reportStatus.vue'; import ExecutionStatus from '@/views/api-test/report/component/reportStatus.vue';
import useOpenNewPage from '@/hooks/useOpenNewPage'; import useOpenNewPage from '@/hooks/useOpenNewPage';
@ -69,6 +76,8 @@
import { SpecialColumnEnum, TableKeyEnum } from '@/enums/tableEnum'; import { SpecialColumnEnum, TableKeyEnum } from '@/enums/tableEnum';
import { FilterSlotNameEnum } from '@/enums/tableFilterEnum'; import { FilterSlotNameEnum } from '@/enums/tableFilterEnum';
import type { moduleKeysType } from './types';
import useModuleSelection from './useModuleSelection';
import { getPublicLinkCaseListMap } from './utils/page'; import { getPublicLinkCaseListMap } from './utils/page';
import { casePriorityOptions } from '@/views/api-test/components/config'; import { casePriorityOptions } from '@/views/api-test/components/config';
@ -87,6 +96,7 @@
getPageApiType: keyof typeof CasePageApiTypeEnum; // Api getPageApiType: keyof typeof CasePageApiTypeEnum; // Api
extraTableParams?: TableQueryParams; // extraTableParams?: TableQueryParams; //
protocols: string[]; protocols: string[];
moduleTree: MsTreeNodeData[];
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
@ -96,6 +106,10 @@
(e: 'update:selectedIds'): void; (e: 'update:selectedIds'): void;
}>(); }>();
const innerSelectedModulesMaps = defineModel<Record<string, moduleKeysType>>('selectedModulesMaps', {
required: true,
});
const tableStore = useTableStore(); const tableStore = useTableStore();
const lastReportStatusListOptions = computed(() => { const lastReportStatusListOptions = computed(() => {
@ -332,6 +346,22 @@
}); });
} }
const { rowSelectChange, selectAllChange, clearSelector, setModuleTree } = useModuleSelection(
innerSelectedModulesMaps.value,
propsRes.value,
props.moduleTree
);
watch(
() => props.moduleTree,
(val) => {
setModuleTree(val);
},
{
immediate: true,
}
);
defineExpose({ defineExpose({
getApiCaseSaveParams, getApiCaseSaveParams,
loadCaseList, loadCaseList,

View File

@ -10,6 +10,9 @@
}" }"
v-on="propsEvent" v-on="propsEvent"
@filter-change="getModuleCount" @filter-change="getModuleCount"
@row-select-change="rowSelectChange"
@select-all-change="selectAllChange"
@clear-selector="clearSelector"
> >
<template #num="{ record }"> <template #num="{ record }">
<MsButton type="text" @click="toDetail(record)">{{ record.num }}</MsButton> <MsButton type="text" @click="toDetail(record)">{{ record.num }}</MsButton>
@ -28,6 +31,9 @@
<div class="one-line-text">{{ record.createUserName }}</div> <div class="one-line-text">{{ record.createUserName }}</div>
</a-tooltip> </a-tooltip>
</template> </template>
<template #count>
<slot></slot>
</template>
</MsBaseTable> </MsBaseTable>
</template> </template>
@ -36,6 +42,7 @@
import MsBaseTable from '@/components/pure/ms-table/base-table.vue'; import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import { MsTableColumn } from '@/components/pure/ms-table/type'; import { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable'; import useTable from '@/components/pure/ms-table/useTable';
import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
import apiMethodName from '@/views/api-test/components/apiMethodName.vue'; import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
@ -52,6 +59,8 @@
import { SpecialColumnEnum, TableKeyEnum } from '@/enums/tableEnum'; import { SpecialColumnEnum, TableKeyEnum } from '@/enums/tableEnum';
import { FilterRemoteMethodsEnum, FilterSlotNameEnum } from '@/enums/tableFilterEnum'; import { FilterRemoteMethodsEnum, FilterSlotNameEnum } from '@/enums/tableFilterEnum';
import type { moduleKeysType } from './types';
import useModuleSelection from './useModuleSelection';
import { getPublicLinkCaseListMap } from './utils/page'; import { getPublicLinkCaseListMap } from './utils/page';
const { t } = useI18n(); const { t } = useI18n();
@ -71,6 +80,7 @@
getPageApiType: keyof typeof CasePageApiTypeEnum; // Api getPageApiType: keyof typeof CasePageApiTypeEnum; // Api
extraTableParams?: TableQueryParams; // extraTableParams?: TableQueryParams; //
protocols: string[]; protocols: string[];
moduleTree: MsTreeNodeData[];
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
@ -81,6 +91,9 @@
}>(); }>();
const tableStore = useTableStore(); const tableStore = useTableStore();
const innerSelectedModulesMaps = defineModel<Record<string, moduleKeysType>>('selectedModulesMaps', {
required: true,
});
const requestMethodsOptions = computed(() => { const requestMethodsOptions = computed(() => {
return Object.values(RequestMethods).map((e) => { return Object.values(RequestMethods).map((e) => {
@ -180,10 +193,10 @@
propsEvent, propsEvent,
loadList, loadList,
setLoadListParams, setLoadListParams,
resetSelector,
setPagination, setPagination,
resetFilterParams, resetFilterParams,
setTableSelected, setTableSelected,
resetSelector,
} = useTable(getPublicLinkCaseListMap[props.getPageApiType][props.activeSourceType].API, { } = useTable(getPublicLinkCaseListMap[props.getPageApiType][props.activeSourceType].API, {
tableKey: TableKeyEnum.ASSOCIATE_CASE_API, tableKey: TableKeyEnum.ASSOCIATE_CASE_API,
showSetting: true, showSetting: true,
@ -239,7 +252,6 @@
setPagination({ setPagination({
current: 1, current: 1,
}); });
resetSelector();
resetFilterParams(); resetFilterParams();
loadApiList(); loadApiList();
} }
@ -299,6 +311,22 @@
}); });
} }
const { rowSelectChange, selectAllChange, clearSelector, setModuleTree } = useModuleSelection(
innerSelectedModulesMaps.value,
propsRes.value,
props.moduleTree
);
watch(
() => props.moduleTree,
(val) => {
setModuleTree(val);
},
{
immediate: true,
}
);
defineExpose({ defineExpose({
getApiSaveParams, getApiSaveParams,
loadApiList, loadApiList,

View File

@ -9,6 +9,9 @@
}" }"
v-on="propsEvent" v-on="propsEvent"
@filter-change="getModuleCount" @filter-change="getModuleCount"
@row-select-change="rowSelectChange"
@select-all-change="selectAllChange"
@clear-selector="clearSelector"
> >
<template #num="{ record }"> <template #num="{ record }">
<MsButton type="text" @click="toDetail(record)">{{ record.num }}</MsButton> <MsButton type="text" @click="toDetail(record)">{{ record.num }}</MsButton>
@ -38,6 +41,9 @@
<template #lastExecResult="{ record }"> <template #lastExecResult="{ record }">
<ExecuteResult :execute-result="record.lastExecResult" /> <ExecuteResult :execute-result="record.lastExecResult" />
</template> </template>
<template #count>
<slot></slot>
</template>
</MsBaseTable> </MsBaseTable>
</template> </template>
@ -51,6 +57,7 @@
import useTable from '@/components/pure/ms-table/useTable'; import useTable from '@/components/pure/ms-table/useTable';
import CaseLevel from '@/components/business/ms-case-associate/caseLevel.vue'; import CaseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
import ExecuteResult from '@/components/business/ms-case-associate/executeResult.vue'; import ExecuteResult from '@/components/business/ms-case-associate/executeResult.vue';
import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
import useOpenNewPage from '@/hooks/useOpenNewPage'; import useOpenNewPage from '@/hooks/useOpenNewPage';
import useTableStore from '@/hooks/useTableStore'; import useTableStore from '@/hooks/useTableStore';
@ -63,6 +70,8 @@
import { SpecialColumnEnum, TableKeyEnum } from '@/enums/tableEnum'; import { SpecialColumnEnum, TableKeyEnum } from '@/enums/tableEnum';
import { FilterSlotNameEnum } from '@/enums/tableFilterEnum'; import { FilterSlotNameEnum } from '@/enums/tableFilterEnum';
import type { moduleKeysType } from './types';
import useModuleSelection from './useModuleSelection';
import { getPublicLinkCaseListMap } from './utils/page'; import { getPublicLinkCaseListMap } from './utils/page';
import { casePriorityOptions } from '@/views/api-test/components/config'; import { casePriorityOptions } from '@/views/api-test/components/config';
import { executionResultMap, statusIconMap } from '@/views/case-management/caseManagementFeature/components/utils'; import { executionResultMap, statusIconMap } from '@/views/case-management/caseManagementFeature/components/utils';
@ -77,6 +86,7 @@
associatedIds?: string[]; // ids associatedIds?: string[]; // ids
activeSourceType: keyof typeof CaseLinkEnum; activeSourceType: keyof typeof CaseLinkEnum;
keyword: string; keyword: string;
moduleTree: MsTreeNodeData[];
getPageApiType: keyof typeof CasePageApiTypeEnum; // Api getPageApiType: keyof typeof CasePageApiTypeEnum; // Api
extraTableParams?: TableQueryParams; // extraTableParams?: TableQueryParams; //
}>(); }>();
@ -86,12 +96,17 @@
(e: 'refresh'): void; (e: 'refresh'): void;
(e: 'initModules'): void; (e: 'initModules'): void;
(e: 'update:selectedIds'): void; (e: 'update:selectedIds'): void;
(e: 'clearSelect'): void;
}>(); }>();
const tableStore = useTableStore(); const tableStore = useTableStore();
const innerSelectedIds = defineModel<string[]>('selectedIds', { required: true }); const innerSelectedIds = defineModel<string[]>('selectedIds', { required: true });
const innerSelectedModulesMaps = defineModel<Record<string, moduleKeysType>>('selectedModulesMaps', {
required: true,
});
const reviewResultOptions = computed(() => { const reviewResultOptions = computed(() => {
return Object.keys(statusIconMap).map((key) => { return Object.keys(statusIconMap).map((key) => {
return { return {
@ -218,8 +233,7 @@
return undefined; return undefined;
} }
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector, resetFilterParams, setTableSelected } = const { propsRes, propsEvent, loadList, setLoadListParams, resetFilterParams, setTableSelected } = useTable(
useTable(
getPageList.value, getPageList.value,
{ {
tableKey: TableKeyEnum.ASSOCIATE_CASE, tableKey: TableKeyEnum.ASSOCIATE_CASE,
@ -253,6 +267,7 @@
async function getModuleCount() { async function getModuleCount() {
const tableParams = await getTableParams(); const tableParams = await getTableParams();
// count
emit('getModuleCount', { emit('getModuleCount', {
...tableParams, ...tableParams,
current: propsRes.value.msPagination?.current, current: propsRes.value.msPagination?.current,
@ -301,6 +316,22 @@
return [...propsRes.value.selectedKeys]; return [...propsRes.value.selectedKeys];
}); });
const { rowSelectChange, selectAllChange, clearSelector, setModuleTree } = useModuleSelection(
innerSelectedModulesMaps.value,
propsRes.value,
props.moduleTree
);
watch(
() => props.moduleTree,
(val) => {
setModuleTree(val);
},
{
immediate: true,
}
);
watch( watch(
() => selectIds.value, () => selectIds.value,
(val) => { (val) => {
@ -309,7 +340,6 @@
); );
watch([() => props.currentProject, () => props.activeModule], () => { watch([() => props.currentProject, () => props.activeModule], () => {
resetSelector();
resetFilterParams(); resetFilterParams();
loadCaseList(); loadCaseList();
}); });

View File

@ -28,10 +28,21 @@
</a-button> </a-button>
</a-tooltip> </a-tooltip>
</div> </div>
<a-spin class="w-full" :loading="moduleLoading"> <div class="flex items-center justify-between">
<a-checkbox v-model:model-value="isCheckAll" :indeterminate="indeterminate" @change="handleChangeAll">{{
t('ms.case.associate.allData')
}}</a-checkbox>
<span class="pr-[8px] text-[var(--color-text-brand)]">
{{ allCount }}
</span>
</div>
<a-spin class="w-full pl-[8px]" :loading="moduleLoading">
<MsTree <MsTree
v-model:selected-keys="selectedKeys" v-model:selected-keys="selectedKeys"
:data="caseTree" v-model:checked-keys="checkedKeys"
v-model:halfCheckedKeys="halfCheckedKeys"
v-model:isCheckAll="isCheckAll"
v-model:data="caseTree"
:keyword="moduleKeyword" :keyword="moduleKeyword"
:empty-text="t('common.noData')" :empty-text="t('common.noData')"
:virtual-list-props="virtualListProps" :virtual-list-props="virtualListProps"
@ -44,13 +55,29 @@
:expand-all="isExpandAll" :expand-all="isExpandAll"
block-node block-node
title-tooltip-position="top" title-tooltip-position="top"
checkable
check-strictly
@select="folderNodeSelect" @select="folderNodeSelect"
@check="checkNode"
> >
<template #title="nodeData"> <template #title="nodeData">
<div class="inline-flex w-full gap-[8px]"> <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="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 class="ms-tree-node-count ml-[4px] flex items-center text-[var(--color-text-brand)]">
{{ nodeData.count || 0 }}
</div> </div>
</div>
</template>
<template #extra="nodeData">
<MsButton
v-if="nodeData.children && nodeData.children.length"
@click="selectCurrent(nodeData, !!checkedKeys.includes(nodeData.id))"
>{{
checkedKeys.includes(nodeData.id)
? t('ms.case.associate.cancelCurrent')
: t('ms.case.associate.selectCurrent')
}}</MsButton
>
</template> </template>
</MsTree> </MsTree>
</a-spin> </a-spin>
@ -60,6 +87,7 @@
import { ref } from 'vue'; import { ref } from 'vue';
import { useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsTree from '@/components/business/ms-tree/index.vue'; import MsTree from '@/components/business/ms-tree/index.vue';
import type { MsTreeNodeData } from '@/components/business/ms-tree/types'; import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
import TreeFolderAll from '@/views/api-test/components/treeFolderAll.vue'; import TreeFolderAll from '@/views/api-test/components/treeFolderAll.vue';
@ -76,7 +104,7 @@
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps<{ const props = defineProps<{
modulesCount?: Record<string, number>; // modulesCount: Record<string, number>; //
selectedKeys: string[]; // key selectedKeys: string[]; // key
currentProject: string; currentProject: string;
getModulesApiType: CaseModulesApiTypeEnum[keyof CaseModulesApiTypeEnum]; getModulesApiType: CaseModulesApiTypeEnum[keyof CaseModulesApiTypeEnum];
@ -91,9 +119,25 @@
(e: 'init', params: ModuleTreeNode[], selectedProtocols?: string[]): void; (e: 'init', params: ModuleTreeNode[], selectedProtocols?: string[]): void;
(e: 'changeProtocol', selectedProtocols: string[]): void; (e: 'changeProtocol', selectedProtocols: string[]): void;
(e: 'update:selectedKeys', selectedKeys: string[]): void; (e: 'update:selectedKeys', selectedKeys: string[]): void;
(e: 'update:halfCheckedKeys', halfCheckedKeys: string[]): void;
(e: 'check', _checkedKeys: Array<string | number>, checkedNodes: MsTreeNodeData): void;
(e: 'selectParent', node: MsTreeNodeData, isSelected: boolean): void;
(e: 'checkAllModule', isCheckedAll: boolean): void;
}>(); }>();
const selectedKeys = useVModel(props, 'selectedKeys', emit); const selectedKeys = useVModel(props, 'selectedKeys', emit);
const checkedKeys = defineModel<(string | number)[]>('checkedKeys', {
required: true,
});
const halfCheckedKeys = defineModel<(string | number)[]>('halfCheckedKeys', {
required: true,
});
const isCheckAll = defineModel<boolean>('isCheckedAll', {
required: true,
});
const indeterminate = defineModel<boolean>('indeterminate', {
required: true,
});
const moduleKeyword = ref(''); const moduleKeyword = ref('');
const activeFolder = ref<string>('all'); const activeFolder = ref<string>('all');
const allCount = ref(0); const allCount = ref(0);
@ -103,7 +147,7 @@
const virtualListProps = computed(() => { const virtualListProps = computed(() => {
return { return {
height: 'calc(100vh - 180px)', height: 'calc(100vh - 236px)',
threshold: 200, threshold: 200,
fixedSize: true, fixedSize: true,
buffer: 15, // 10 padding buffer: 15, // 10 padding
@ -129,6 +173,42 @@
} }
const selectedProtocols = ref<string[]>([]); const selectedProtocols = ref<string[]>([]);
// count
function processTreeData(nodes: MsTreeNodeData[]): MsTreeNodeData[] {
const traverse = (node: MsTreeNodeData): number => {
let totalChildrenCount = 0;
if (node.children && node.children.length > 0) {
totalChildrenCount = node.children.reduce((sum, child) => {
return sum + traverse(child);
}, 0);
node.count -= totalChildrenCount;
}
return node.count + totalChildrenCount;
};
nodes.forEach((node: MsTreeNodeData) => traverse(node));
return nodes;
}
function calculateTreeCount(treeData: MsTreeNodeData[]) {
caseTree.value = mapTree<ModuleTreeNode>(treeData, (node) => {
return {
...node,
count: props.modulesCount[node.id],
};
});
const updatedModuleTreeCount = processTreeData(caseTree.value) as MsTreeNodeData[];
caseTree.value = mapTree<ModuleTreeNode>(updatedModuleTreeCount, (node) => {
return {
...node,
count: node.count,
};
});
allCount.value = props.modulesCount.all || 0;
}
/** /**
* 初始化模块树 * 初始化模块树
*/ */
@ -143,15 +223,13 @@
}; };
const res = await getModuleTreeFunc(props.getModulesApiType, props.activeTab, getModuleParams); const res = await getModuleTreeFunc(props.getModulesApiType, props.activeTab, getModuleParams);
caseTree.value = mapTree<ModuleTreeNode>(res, (node) => {
return { calculateTreeCount(res);
...node,
count: props.modulesCount?.[node.id] || 0,
};
});
if (setDefault) { if (setDefault) {
setActiveFolder('all'); setActiveFolder('all');
} }
emit('init', caseTree.value, selectedProtocols.value); emit('init', caseTree.value, selectedProtocols.value);
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
@ -166,22 +244,33 @@
initModules(); initModules();
} }
function checkNode(_checkedKeys: Array<string | number>, checkedNodes: MsTreeNodeData) {
emit('check', _checkedKeys, checkedNodes);
}
function selectCurrent(node: MsTreeNodeData, isSelected: boolean) {
emit('selectParent', node, isSelected);
}
/** /**
* 初始化模块文件数量 * 初始化模块文件数量
*/ */
watch( watch(
() => props.modulesCount, () => props.modulesCount,
(obj) => { () => {
caseTree.value = mapTree<ModuleTreeNode>(caseTree.value, (node) => { calculateTreeCount(caseTree.value);
return { },
...node, {
count: obj?.[node.id] || 0, deep: true,
}; immediate: true,
});
allCount.value = obj?.all || 0;
} }
); );
function handleChangeAll(value: boolean | (string | number | boolean)[], ev: Event) {
isCheckAll.value = value as boolean;
emit('checkAllModule', isCheckAll.value);
}
watch( watch(
() => props.showType, () => props.showType,
(val) => { (val) => {

View File

@ -113,11 +113,15 @@
</a-input-group> </a-input-group>
</div> </div>
</template> </template>
<div class="flex h-[calc(100vh-58px)]"> <div class="flex h-[calc(100vh-118px)]">
<div class="w-[292px] border-r border-[var(--color-text-n8)] p-[16px]"> <div class="w-[292px] border-r border-[var(--color-text-n8)] p-[16px]">
<CaseTree <CaseTree
ref="caseTreeRef" ref="caseTreeRef"
v-model:checkedKeys="checkedKeys"
v-model:selected-keys="selectedKeys" v-model:selected-keys="selectedKeys"
v-model:halfCheckedKeys="halfCheckedKeys"
v-model:isCheckedAll="isCheckedAll"
v-model:indeterminate="indeterminate"
:modules-count="modulesCount" :modules-count="modulesCount"
:get-modules-api-type="props.getModulesApiType" :get-modules-api-type="props.getModulesApiType"
:current-project="innerProject" :current-project="innerProject"
@ -128,9 +132,12 @@
@folder-node-select="handleFolderNodeSelect" @folder-node-select="handleFolderNodeSelect"
@init="initModuleTree" @init="initModuleTree"
@change-protocol="handleProtocolChange" @change-protocol="handleProtocolChange"
@select-parent="selectParent"
@check="checkNode"
@check-all-module="checkAllModule"
/> />
</div> </div>
<div class="flex w-[calc(100%-293px)] flex-col p-[16px]"> <div class="relative flex w-[calc(100%-293px)] flex-col p-[16px]">
<MsAdvanceFilter <MsAdvanceFilter
v-model:keyword="keyword" v-model:keyword="keyword"
:filter-config-list="[]" :filter-config-list="[]"
@ -166,18 +173,6 @@
</div> </div>
</template> </template>
</a-popover> </a-popover>
<!-- TODO 正式版暂时不上了 -->
<!-- <a-checkbox v-if="associationType === '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> </div>
</template> </template>
</MsAdvanceFilter> </MsAdvanceFilter>
@ -186,6 +181,7 @@
v-if="associationType === CaseLinkEnum.FUNCTIONAL" v-if="associationType === CaseLinkEnum.FUNCTIONAL"
ref="functionalTableRef" ref="functionalTableRef"
v-model:selectedIds="selectedIds" v-model:selectedIds="selectedIds"
v-model:selectedModulesMaps="selectedModulesMaps"
:association-type="associateType" :association-type="associateType"
:get-page-api-type="getPageApiType" :get-page-api-type="getPageApiType"
:active-module="activeFolder" :active-module="activeFolder"
@ -195,14 +191,18 @@
:active-source-type="associationType" :active-source-type="associationType"
:extra-table-params="props.extraTableParams" :extra-table-params="props.extraTableParams"
:keyword="keyword" :keyword="keyword"
:module-tree="moduleTree"
@get-module-count="initModulesCount" @get-module-count="initModulesCount"
@refresh="loadCaseList" @refresh="loadCaseList"
/> >
<TotalCount :total-count="totalCount" />
</CaseTable>
<!-- 接口用例 API --> <!-- 接口用例 API -->
<ApiTable <ApiTable
v-if="associationType === CaseLinkEnum.API && showType === 'API'" v-if="associationType === CaseLinkEnum.API && showType === 'API'"
ref="apiTableRef" ref="apiTableRef"
v-model:selectedIds="selectedIds" v-model:selectedIds="selectedIds"
v-model:selectedModulesMaps="selectedModulesMaps"
:get-page-api-type="getPageApiType" :get-page-api-type="getPageApiType"
:extra-table-params="props.extraTableParams" :extra-table-params="props.extraTableParams"
:association-type="associateType" :association-type="associateType"
@ -214,13 +214,17 @@
:keyword="keyword" :keyword="keyword"
:show-type="showType" :show-type="showType"
:protocols="selectedProtocols" :protocols="selectedProtocols"
:module-tree="moduleTree"
@get-module-count="initModulesCount" @get-module-count="initModulesCount"
/> >
<TotalCount :total-count="totalCount" />
</ApiTable>
<!-- 接口用例 CASE --> <!-- 接口用例 CASE -->
<ApiCaseTable <ApiCaseTable
v-if="associationType === CaseLinkEnum.API && showType === 'CASE'" v-if="associationType === CaseLinkEnum.API && showType === 'CASE'"
ref="caseTableRef" ref="caseTableRef"
v-model:selectedIds="selectedIds" v-model:selectedIds="selectedIds"
v-model:selectedModulesMaps="selectedModulesMaps"
:get-page-api-type="getPageApiType" :get-page-api-type="getPageApiType"
:extra-table-params="props.extraTableParams" :extra-table-params="props.extraTableParams"
:association-type="associateType" :association-type="associateType"
@ -232,12 +236,16 @@
:keyword="keyword" :keyword="keyword"
:show-type="showType" :show-type="showType"
:protocols="selectedProtocols" :protocols="selectedProtocols"
:module-tree="moduleTree"
@get-module-count="initModulesCount" @get-module-count="initModulesCount"
/> >
<TotalCount :total-count="totalCount" />
</ApiCaseTable>
<!-- 接口场景用例 --> <!-- 接口场景用例 -->
<ScenarioCaseTable <ScenarioCaseTable
v-if="associationType === CaseLinkEnum.SCENARIO" v-if="associationType === CaseLinkEnum.SCENARIO"
ref="scenarioTableRef" ref="scenarioTableRef"
v-model:selectedModulesMaps="selectedModulesMaps"
v-model:selectedIds="selectedIds" v-model:selectedIds="selectedIds"
:association-type="associateType" :association-type="associateType"
:modules-count="modulesCount" :modules-count="modulesCount"
@ -249,13 +257,70 @@
:get-page-api-type="getPageApiType" :get-page-api-type="getPageApiType"
:extra-table-params="props.extraTableParams" :extra-table-params="props.extraTableParams"
:keyword="keyword" :keyword="keyword"
:module-tree="moduleTree"
:total-count="totalCount"
@get-module-count="initModulesCount" @get-module-count="initModulesCount"
@refresh="loadCaseList" @refresh="loadCaseList"
>
<TotalCount :total-count="totalCount" />
</ScenarioCaseTable>
</div>
</div>
<div class="footer !ml-[10px] w-[calc(100%-10px)]">
<div class="flex items-center">
<slot name="footerLeft">
<div v-if="props.associatedType === CaseLinkEnum.FUNCTIONAL" class="flex items-center">
<a-switch v-model:model-value="syncCase" size="small" type="line" />
<div class="ml-[8px]">{{ t('ms.case.associate.syncFunctionalCase') }}</div>
<a-tooltip :content="t('ms.case.associate.addAutomaticallyCase')" position="top">
<icon-question-circle
class="ml-[4px] text-[var(--color-text-brand)] hover:text-[rgb(var(--primary-5))]"
size="16"
/> />
</a-tooltip>
</div>
<div v-if="props.associatedType === CaseLinkEnum.FUNCTIONAL" class="ml-[16px] flex items-center">
<a-tree-select
v-model="apiCaseCollectionId"
:field-names="{
title: 'name',
key: 'id',
children: 'children',
}"
class="w-[200px]"
:data="apiSetTree"
>
<template #prefix>
<div class="text-[var(--color-text-brand)]">{{ t('ms.case.associate.api') }}</div>
</template>
<template #tree-slot-title="node">
<a-tooltip :content="`${node.name}`" position="tl">
<div class="one-line-text w-[180px]">{{ node.name }}</div>
</a-tooltip>
</template>
</a-tree-select>
<div class="footer"> <a-tree-select
<div class="flex flex-1 items-center"> v-model="apiScenarioCollectionId"
<slot name="footerLeft"></slot> :field-names="{
title: 'name',
key: 'id',
children: 'children',
}"
class="ml-[12px] w-[200px]"
:data="scenarioSetTree"
>
<template #prefix>
<div class="text-[var(--color-text-brand)]">{{ t('ms.case.associate.scenario') }}</div>
</template>
<template #tree-slot-title="node">
<a-tooltip :content="`${node.name}`" position="tl">
<div class="one-line-text w-[180px]">{{ node.name }}</div>
</a-tooltip>
</template>
</a-tree-select>
</div>
</slot>
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<slot name="footerRight"> <slot name="footerRight">
@ -265,7 +330,7 @@
<a-button <a-button
:loading="props.confirmLoading" :loading="props.confirmLoading"
type="primary" type="primary"
:disabled="!selectedIds.length" :disabled="!isDisabledSaveButton"
@click="handleConfirm" @click="handleConfirm"
> >
{{ t('ms.case.associate.associate') }} {{ t('ms.case.associate.associate') }}
@ -273,8 +338,6 @@
</slot> </slot>
</div> </div>
</div> </div>
</div>
</div>
</MsDrawer> </MsDrawer>
</template> </template>
@ -284,13 +347,16 @@
import { MsAdvanceFilter } from '@/components/pure/ms-advance-filter'; import { MsAdvanceFilter } from '@/components/pure/ms-advance-filter';
import MsDrawer from '@/components/pure/ms-drawer/index.vue'; import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
import ApiCaseTable from './apiCaseTable.vue'; import ApiCaseTable from './apiCaseTable.vue';
import ApiTable from './apiTable.vue'; import ApiTable from './apiTable.vue';
import CaseTable from './caseTable.vue'; import CaseTable from './caseTable.vue';
import CaseTree from './caseTree.vue'; import CaseTree from './caseTree.vue';
import ScenarioCaseTable from './scenarioCaseTable.vue'; import ScenarioCaseTable from './scenarioCaseTable.vue';
import TotalCount from './totalCount.vue';
import { getAssociatedProjectOptions } from '@/api/modules/case-management/featureCase'; import { getAssociatedProjectOptions } from '@/api/modules/case-management/featureCase';
import { getApiCaseModule, getApiScenarioModule } from '@/api/modules/test-plan/testPlan';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useVisit from '@/hooks/useVisit'; import useVisit from '@/hooks/useVisit';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
@ -300,10 +366,10 @@
import { CaseModulesApiTypeEnum, CasePageApiTypeEnum } from '@/enums/associateCaseEnum'; import { CaseModulesApiTypeEnum, CasePageApiTypeEnum } from '@/enums/associateCaseEnum';
import { CaseLinkEnum } from '@/enums/caseEnum'; import { CaseLinkEnum } from '@/enums/caseEnum';
import type { moduleKeysType, saveParams } from './types';
import { initGetModuleCountFunc } from './utils/moduleCount'; import { initGetModuleCountFunc } from './utils/moduleCount';
const visitedKey = 'changeLinkProject'; const visitedKey = 'changeLinkProject';
const { addVisited, getIsVisited } = useVisit(visitedKey); const { addVisited, getIsVisited } = useVisit(visitedKey);
const appStore = useAppStore(); const appStore = useAppStore();
@ -321,6 +387,7 @@
extraModuleCountParams?: TableQueryParams; // extraModuleCountParams?: TableQueryParams; //
okButtonDisabled?: boolean; // okButtonDisabled?: boolean; //
confirmLoading?: boolean; confirmLoading?: boolean;
modulesMaps?: Record<string, saveParams>;
associatedIds?: string[]; // id associatedIds?: string[]; // id
hideProjectSelect?: boolean; // hideProjectSelect?: boolean; //
associatedType: keyof typeof CaseLinkEnum; // associatedType: keyof typeof CaseLinkEnum; //
@ -351,11 +418,24 @@
const activeFolder = ref('all'); const activeFolder = ref('all');
const selectedIds = ref<string[]>([]); const selectedIds = ref<string[]>([]);
const checkedKeys = ref<Array<string | number>>([]);
const halfCheckedKeys = ref<Array<string | number>>([]);
//
const selectedModulesMaps = ref<Record<string, moduleKeysType>>({});
//
const isDisabledSaveButton = computed(() => {
return Object.values(selectedModulesMaps.value).some((module) => module.selectAll || module.selectIds.size > 0);
});
const selectedKeys = computed({ const selectedKeys = computed({
get: () => [activeFolder.value], get: () => [activeFolder.value],
set: (val) => val, set: (val) => val,
}); });
const syncCase = ref<boolean>(true);
const folderName = computed(() => { const folderName = computed(() => {
switch (associationType.value) { switch (associationType.value) {
case CaseLinkEnum.FUNCTIONAL: case CaseLinkEnum.FUNCTIONAL:
@ -399,26 +479,54 @@
const caseTableRef = ref<InstanceType<typeof ApiCaseTable>>(); const caseTableRef = ref<InstanceType<typeof ApiCaseTable>>();
const scenarioTableRef = ref<InstanceType<typeof ScenarioCaseTable>>(); const scenarioTableRef = ref<InstanceType<typeof ScenarioCaseTable>>();
function makeParams() { function getMapParams() {
switch (props.associatedType) { const selectedParams: Record<string, saveParams> = {};
case CaseLinkEnum.FUNCTIONAL: Object.entries(selectedModulesMaps.value).forEach(([moduleId, selectedProps]) => {
return functionalTableRef.value?.getFunctionalSaveParams(); const { selectAll, selectIds, excludeIds, count } = selectedProps;
case CaseLinkEnum.API: selectedParams[moduleId] = {
return showType.value === 'API' count,
? apiTableRef.value?.getApiSaveParams() selectAll,
: caseTableRef.value?.getApiCaseSaveParams(); selectIds: [...selectIds],
case CaseLinkEnum.SCENARIO: excludeIds: [...excludeIds],
return scenarioTableRef.value?.getScenarioSaveParams(); };
default: });
break; return selectedParams;
} }
const isCheckedAll = ref<boolean>(false);
const indeterminate = ref<boolean>(false);
const totalCount = computed(() => {
if (isCheckedAll.value) {
return modulesCount.value.all;
} }
return Object.values(selectedModulesMaps.value).reduce((total, module) => {
return total + (module.selectAll ? module.count : module.selectIds.size);
}, 0);
});
const apiCaseCollectionId = ref<string>('');
const apiScenarioCollectionId = ref<string>('');
// //
function handleConfirm() { function handleConfirm() {
const params = makeParams(); const params = {
if (!params?.selectIds.length) { moduleMaps: getMapParams(),
return; syncCase: syncCase.value,
apiCaseCollectionId: apiCaseCollectionId.value,
apiScenarioCollectionId: apiScenarioCollectionId.value,
selectAllModule: isCheckedAll.value,
projectId: innerProject.value,
associateType: 'FUNCTIONAL',
totalCount: totalCount.value,
};
if (params.associateType === CaseLinkEnum.API) {
params.associateType = showType.value;
} else {
params.associateType = props.associatedType;
} }
emit('save', params); emit('save', params);
} }
@ -445,11 +553,15 @@
console.log(error); console.log(error);
} }
} }
const selectedProtocols = ref<string[]>([]); const selectedProtocols = ref<string[]>([]);
async function initModulesCount(params: TableQueryParams) { async function initModulesCount(params: TableQueryParams) {
try { try {
modulesCount.value = await initGetModuleCountFunc(props.getModuleCountApiType, associationType.value, { modulesCount.value = await initGetModuleCountFunc(props.getModuleCountApiType, associationType.value, {
...params, ...params,
moduleIds: [],
filter: {},
keyword: '',
...props.extraModuleCountParams, ...props.extraModuleCountParams,
protocols: associationType.value === CaseLinkEnum.API ? selectedProtocols.value : undefined, protocols: associationType.value === CaseLinkEnum.API ? selectedProtocols.value : undefined,
}); });
@ -480,20 +592,32 @@
const moduleTree = ref<ModuleTreeNode[]>([]); const moduleTree = ref<ModuleTreeNode[]>([]);
function initModuleTree(tree: ModuleTreeNode[], _protocols?: string[]) { function initModuleTree(tree: ModuleTreeNode[], _protocols?: string[]) {
moduleTree.value = unref(tree); moduleTree.value = tree;
selectedProtocols.value = _protocols || []; selectedProtocols.value = _protocols || [];
if (props.associatedType === CaseLinkEnum.API) { if (props.associatedType === CaseLinkEnum.API) {
loadCaseList(); loadCaseList();
} }
} }
const functionalType = ref('project'); const apiSetTree = ref<ModuleTreeNode[]>();
const functionalList = ref([ const scenarioSetTree = ref<ModuleTreeNode[]>();
{
id: 'project', async function initTestSet() {
name: t('ms.case.associate.project'), if (props.extraTableParams?.testPlanId) {
}, try {
]); apiSetTree.value = await getApiCaseModule({
testPlanId: props.extraTableParams.testPlanId,
treeType: 'COLLECTION',
});
scenarioSetTree.value = await getApiScenarioModule({
testPlanId: props.extraTableParams.testPlanId,
treeType: 'COLLECTION',
});
} catch (error) {
console.log(error);
}
}
}
function changeProjectHandler(visible: boolean) { function changeProjectHandler(visible: boolean) {
if (visible && !getIsVisited()) { if (visible && !getIsVisited()) {
@ -527,6 +651,7 @@
associationType.value = props.associatedType; associationType.value = props.associatedType;
activeFolder.value = 'all'; activeFolder.value = 'all';
initProjectList(); initProjectList();
initTestSet();
} }
selectPopVisible.value = false; selectPopVisible.value = false;
keyword.value = ''; keyword.value = '';
@ -542,6 +667,142 @@
} }
} }
); );
// &&
function selectParent(nodeData: MsTreeNodeData, isSelected: boolean) {
selectedModulesMaps.value[nodeData.id] = {
selectAll: !isSelected,
selectIds: new Set(),
excludeIds: new Set(),
count: nodeData.count,
};
}
//
function processAllCurrentNode(node: MsTreeNodeData, check: boolean) {
if (node.children && node.children.length) {
node.children?.forEach((childrenNode: MsTreeNodeData) => processAllCurrentNode(childrenNode, check));
}
selectedModulesMaps.value[node.id] = {
selectAll: check,
selectIds: new Set(),
excludeIds: new Set(),
count: node.count,
};
}
//
function checkNode(_checkedKeys: Array<string | number>, checkedData: MsTreeNodeData) {
const { checked, node } = checkedData;
processAllCurrentNode(node, checked);
}
watch(
() => selectedModulesMaps.value,
(val) => {
const checkedKeysSet = new Set(checkedKeys.value);
const halfCheckedKeysSet = new Set(halfCheckedKeys.value);
if (!Object.keys(val).length) {
checkedKeysSet.clear();
halfCheckedKeysSet.clear();
isCheckedAll.value = false;
indeterminate.value = false;
}
Object.entries(val).forEach(([moduleId, selectedProps]) => {
const { selectAll: selectIdsAll, selectIds, count } = selectedProps;
//
if (selectIdsAll) {
checkedKeysSet.add(moduleId);
} else {
checkedKeysSet.delete(moduleId);
}
//
if (selectIds.size > 0 && selectIds.size < count) {
halfCheckedKeysSet.add(moduleId);
} else {
halfCheckedKeysSet.delete(moduleId);
}
});
// checkedKeys halfCheckedKeys
checkedKeys.value = Array.from(checkedKeysSet);
halfCheckedKeys.value = Array.from(halfCheckedKeysSet);
//
const isAllCheckedModuleProps = val.all;
if (isAllCheckedModuleProps) {
const { selectAll, selectIds, count } = isAllCheckedModuleProps;
isCheckedAll.value = selectAll;
if (selectIds.size > 0 && selectIds.size < count) {
indeterminate.value = true;
} else {
indeterminate.value = false;
}
}
},
{ deep: true }
);
// &
function setSelectAll(tree: MsTreeNodeData[], checkedAll: boolean) {
tree.forEach((node) => {
processAllCurrentNode(node, checkedAll);
if (node.children) {
setSelectAll(node.children, checkedAll);
}
});
}
function checkAllModule() {
selectedModulesMaps.value.all = {
selectAll: isCheckedAll.value,
selectIds: new Set(),
excludeIds: new Set(),
count: modulesCount.value.all,
};
setSelectAll(moduleTree.value, isCheckedAll.value);
}
//
function clearSelector() {
Object.keys(selectedModulesMaps.value).forEach((key) => {
delete selectedModulesMaps.value[key];
});
}
watch(
() => innerProject.value,
(val) => {
if (val) {
clearSelector();
}
}
);
watch(
() => props.modulesMaps,
(val) => {
if (val && Object.keys(val).length) {
Object.entries(val).forEach(([moduleId, selectedProps]) => {
const { selectAll, selectIds, excludeIds, count } = selectedProps;
selectedModulesMaps.value[moduleId] = {
selectAll,
count,
selectIds: new Set(selectIds),
excludeIds: new Set(excludeIds),
};
});
}
}
);
</script> </script>
<style scoped lang="less"> <style scoped lang="less">

View File

@ -22,4 +22,14 @@ export default {
'ms.case.associate.switchProject': 'Switch project?', 'ms.case.associate.switchProject': 'Switch project?',
'ms.case.associate.switchProjectPopTip': 'After switching, the selected data will be cleared', 'ms.case.associate.switchProjectPopTip': 'After switching, the selected data will be cleared',
'ms.case.associate.apiSearchPlaceholder': 'Support ID/ name/tag/ path search', 'ms.case.associate.apiSearchPlaceholder': 'Support ID/ name/tag/ path search',
'ms.case.associate.selectCurrentNode': 'Select current',
'ms.case.associate.syncFunctionalCase': 'Add function in cases of related cases',
'ms.case.associate.addAutomaticallyCase': 'Automatically add associated interface use cases and scenario use cases',
'ms.case.associate.api': 'Api',
'ms.case.associate.scenario': 'Scenario',
'ms.case.associate.selectCurrent': 'Select current',
'ms.case.associate.cancelCurrent': 'Cancel current',
'ms.case.associate.allData': 'All',
'ms.case.associate.allSelected': 'Selected',
'ms.case.associate.allSelectedItem': 'Item data',
}; };

View File

@ -22,4 +22,14 @@ export default {
'ms.case.associate.switchProject': '切换项目?', 'ms.case.associate.switchProject': '切换项目?',
'ms.case.associate.switchProjectPopTip': '切换后,已选数据将清空', 'ms.case.associate.switchProjectPopTip': '切换后,已选数据将清空',
'ms.case.associate.apiSearchPlaceholder': '通过 ID/名称/标签/路径搜索', 'ms.case.associate.apiSearchPlaceholder': '通过 ID/名称/标签/路径搜索',
'ms.case.associate.selectCurrentNode': '选择当前',
'ms.case.associate.syncFunctionalCase': '同步添加功能用例的关联用例',
'ms.case.associate.addAutomaticallyCase': '自动添加已关联的接口用例、场景用例',
'ms.case.associate.api': '接口',
'ms.case.associate.scenario': '场景',
'ms.case.associate.selectCurrent': '选择当前',
'ms.case.associate.cancelCurrent': '取消当前',
'ms.case.associate.allData': '全部',
'ms.case.associate.allSelected': '已选',
'ms.case.associate.allSelectedItem': '项数据',
}; };

View File

@ -9,6 +9,9 @@
}" }"
v-on="propsEvent" v-on="propsEvent"
@filter-change="getModuleCount" @filter-change="getModuleCount"
@row-select-change="rowSelectChange"
@select-all-change="selectAllChange"
@clear-selector="clearSelector"
> >
<template #num="{ record }"> <template #num="{ record }">
<MsButton type="text" @click="toDetail(record)">{{ record.num }}</MsButton> <MsButton type="text" @click="toDetail(record)">{{ record.num }}</MsButton>
@ -34,6 +37,9 @@
<div class="one-line-text">{{ record.createUserName }}</div> <div class="one-line-text">{{ record.createUserName }}</div>
</a-tooltip> </a-tooltip>
</template> </template>
<template #count>
<slot></slot>
</template>
</MsBaseTable> </MsBaseTable>
</template> </template>
@ -45,6 +51,7 @@
import { MsTableColumn } from '@/components/pure/ms-table/type'; import { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable'; import useTable from '@/components/pure/ms-table/useTable';
import CaseLevel from '@/components/business/ms-case-associate/caseLevel.vue'; import CaseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
import ExecutionStatus from '@/views/api-test/report/component/reportStatus.vue'; import ExecutionStatus from '@/views/api-test/report/component/reportStatus.vue';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
@ -61,6 +68,8 @@
import { SpecialColumnEnum, TableKeyEnum } from '@/enums/tableEnum'; import { SpecialColumnEnum, TableKeyEnum } from '@/enums/tableEnum';
import { FilterRemoteMethodsEnum, FilterSlotNameEnum } from '@/enums/tableFilterEnum'; import { FilterRemoteMethodsEnum, FilterSlotNameEnum } from '@/enums/tableFilterEnum';
import type { moduleKeysType } from './types';
import useModuleSelection from './useModuleSelection';
import { getPublicLinkCaseListMap } from './utils/page'; import { getPublicLinkCaseListMap } from './utils/page';
import { casePriorityOptions } from '@/views/api-test/components/config'; import { casePriorityOptions } from '@/views/api-test/components/config';
@ -77,6 +86,7 @@
keyword: string; keyword: string;
getPageApiType: keyof typeof CasePageApiTypeEnum; // Api getPageApiType: keyof typeof CasePageApiTypeEnum; // Api
extraTableParams?: TableQueryParams; // extraTableParams?: TableQueryParams; //
moduleTree: MsTreeNodeData[];
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
@ -89,6 +99,10 @@
const appStore = useAppStore(); const appStore = useAppStore();
const tableStore = useTableStore(); const tableStore = useTableStore();
const innerSelectedModulesMaps = defineModel<Record<string, moduleKeysType>>('selectedModulesMaps', {
required: true,
});
const statusList = computed(() => { const statusList = computed(() => {
return Object.keys(ReportStatus).map((key) => { return Object.keys(ReportStatus).map((key) => {
return { return {
@ -191,8 +205,10 @@
const getPageList = computed(() => { const getPageList = computed(() => {
return getPublicLinkCaseListMap[props.getPageApiType][props.activeSourceType]; return getPublicLinkCaseListMap[props.getPageApiType][props.activeSourceType];
}); });
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector, resetFilterParams, setTableSelected } =
useTable(getPageList.value, { const { propsRes, propsEvent, loadList, setLoadListParams, resetFilterParams, setTableSelected } = useTable(
getPageList.value,
{
tableKey: TableKeyEnum.ASSOCIATE_CASE_API_SCENARIO, tableKey: TableKeyEnum.ASSOCIATE_CASE_API_SCENARIO,
showSetting: true, showSetting: true,
isSimpleSetting: true, isSimpleSetting: true,
@ -201,7 +217,8 @@
showSelectAll: true, showSelectAll: true,
heightUsed: 310, heightUsed: 310,
showSelectorAll: false, showSelectorAll: false,
}); }
);
async function getTableParams() { async function getTableParams() {
const { excludeKeys } = propsRes.value; const { excludeKeys } = propsRes.value;
@ -265,6 +282,22 @@
}; };
} }
const { rowSelectChange, selectAllChange, clearSelector, setModuleTree } = useModuleSelection(
innerSelectedModulesMaps.value,
propsRes.value,
props.moduleTree
);
watch(
() => props.moduleTree,
(val) => {
setModuleTree(val);
},
{
immediate: true,
}
);
// //
function toDetail(record: ApiCaseDetail) { function toDetail(record: ApiCaseDetail) {
openNewPage(ApiTestRouteEnum.API_TEST_SCENARIO, { openNewPage(ApiTestRouteEnum.API_TEST_SCENARIO, {
@ -273,7 +306,6 @@
}); });
} }
watch([() => props.currentProject, () => props.activeModule], () => { watch([() => props.currentProject, () => props.activeModule], () => {
resetSelector();
resetFilterParams(); resetFilterParams();
loadScenarioList(); loadScenarioList();
}); });

View File

@ -0,0 +1,21 @@
<template>
<div class="text-[var(--color-text-2)]">
{{ t('ms.case.associate.allSelected') }}
<span class="mx-[4px] text-[rgb(var(--primary-5))]">{{ props.totalCount }}</span>
{{ t('ms.case.associate.allSelectedItem') }}
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useI18n } from '@/hooks/useI18n';
const { t } = useI18n();
const props = defineProps<{
totalCount: number;
}>();
</script>
<style scoped></style>

View File

@ -0,0 +1,13 @@
export interface moduleKeysType {
selectAll: boolean;
selectIds: Set<string>;
excludeIds: Set<string>;
count: number;
}
export interface saveParams {
selectAll: boolean;
selectIds: string[];
excludeIds: string[];
count: number;
}

View File

@ -0,0 +1,217 @@
import { ref, watch } from 'vue';
import type { MsTableProps } from '@/components/pure/ms-table/type';
import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
import { findNodeByKey } from '@/utils';
import { SelectAllEnum } from '@/enums/tableEnum';
import type { moduleKeysType } from './types';
export default function useModuleSelections<T>(
innerSelectedModulesMaps: Record<string, moduleKeysType>,
propsRes: MsTableProps<T>,
modulesTree: MsTreeNodeData[]
) {
const moduleSelectedMap = ref<Record<string, string[]>>({});
const moduleTree = ref<MsTreeNodeData[]>(modulesTree);
// 初始化表格数据的选择
function initTableDataSelected() {
propsRes.selectedKeys = new Set([]);
propsRes.excludeKeys = new Set([]);
const allSelectIds: Set<string> = new Set();
propsRes.data.forEach((item: any) => {
const selectAllProps = innerSelectedModulesMaps[item.moduleId];
if (
selectAllProps &&
selectAllProps.selectAll &&
!selectAllProps.selectIds.size &&
!selectAllProps.excludeIds.size
) {
(moduleSelectedMap.value[item.moduleId] || []).forEach((id) => allSelectIds.add(id));
} else if (selectAllProps && !selectAllProps.selectAll && selectAllProps.selectIds.size) {
selectAllProps.selectIds.forEach((id) => allSelectIds.add(id));
}
});
propsRes.selectedKeys = new Set([...allSelectIds]);
}
// 初始化模块分类
function initPropsDataSort() {
propsRes.data.forEach((item: any) => {
if (!moduleSelectedMap.value[item.moduleId]) {
moduleSelectedMap.value[item.moduleId] = [item.id];
}
if (!moduleSelectedMap.value[item.moduleId].includes(item.id)) {
moduleSelectedMap.value[item.moduleId].push(item.id);
}
});
}
// 单选或复选时处理全选状态
function setSelectedAll(moduleId: string) {
innerSelectedModulesMaps[moduleId].selectAll = true;
innerSelectedModulesMaps[moduleId].selectIds = new Set([]);
innerSelectedModulesMaps[moduleId].excludeIds = new Set([]);
const selectedProp = innerSelectedModulesMaps[moduleId];
if (selectedProp) {
if (selectedProp.selectAll && !selectedProp.selectIds.size && !selectedProp.excludeIds.size) {
moduleSelectedMap.value[moduleId].forEach((key) => {
propsRes.selectedKeys.add(key);
});
}
}
}
// 初始化节点,防止选择时报错
function setUnSelectNode(moduleId: string, key = 'id') {
if (!innerSelectedModulesMaps[moduleId]) {
const node = findNodeByKey<MsTreeNodeData>(moduleTree.value, moduleId, key);
innerSelectedModulesMaps[moduleId] = {
selectAll: false,
selectIds: new Set(),
excludeIds: new Set(),
count: node?.count || 0,
};
}
}
// 设置最新状态
function setSelectedModuleStatus(moduleId: string) {
const selectedProps = innerSelectedModulesMaps[moduleId];
if (selectedProps) {
const { selectIds: selectModuleIds, count, excludeIds } = selectedProps;
if (selectModuleIds.size < count) {
innerSelectedModulesMaps[moduleId].selectAll = false;
}
if (selectModuleIds.size === count) {
setSelectedAll(moduleId);
}
if (excludeIds.size === count) {
innerSelectedModulesMaps[moduleId].selectAll = false;
innerSelectedModulesMaps[moduleId].selectIds = new Set([]);
innerSelectedModulesMaps[moduleId].excludeIds = new Set([]);
}
}
}
// 更新选择数据
function updateSelectModule(moduleId: string, id: string) {
const selectedProps = innerSelectedModulesMaps[moduleId];
if (selectedProps) {
const selectedSet = selectedProps.selectIds;
const excludedSet = selectedProps.excludeIds;
const isSelectAllModule =
selectedProps.selectAll && !selectedProps.selectIds.size && !selectedProps.excludeIds.size;
if (isSelectAllModule && moduleId === 'all') {
Object.entries(moduleSelectedMap.value).forEach(([item, allSelectIds]) => {
allSelectIds.forEach((key) => {
selectedProps.selectIds.add(key);
});
});
} else if (isSelectAllModule && moduleId !== 'all') {
moduleSelectedMap.value[moduleId].forEach((key) => {
selectedProps.selectIds.add(key);
});
}
if (selectedSet.has(id)) {
selectedProps.excludeIds.add(id);
selectedProps.selectIds.delete(id);
} else if (excludedSet.has(id)) {
selectedProps.excludeIds.delete(id);
selectedProps.selectIds.add(id);
} else if (!selectedSet.has(id) && !excludedSet.has(id)) {
selectedProps.selectIds.add(id);
}
innerSelectedModulesMaps[moduleId] = selectedProps;
setSelectedModuleStatus(moduleId);
}
}
function rowSelectChange(record: Record<string, any>) {
const { moduleId } = record;
setUnSelectNode(moduleId);
updateSelectModule(moduleId, record.id);
updateSelectModule('all', record.id);
}
function selectAllChange(v: SelectAllEnum) {
const { data } = propsRes;
if (v === 'current') {
propsRes.selectedKeys = new Set([]);
data.forEach((item: any) => {
const { moduleId } = item;
setUnSelectNode(moduleId);
const lastSelectedProps = innerSelectedModulesMaps[moduleId];
if (!lastSelectedProps.selectAll) {
innerSelectedModulesMaps[moduleId].selectIds.add(item.id);
innerSelectedModulesMaps[moduleId].excludeIds.delete(item.id);
setSelectedModuleStatus(moduleId);
}
updateSelectModule('all', item.id);
});
} else {
data.forEach((item: any) => {
const { moduleId } = item;
setUnSelectNode(moduleId);
innerSelectedModulesMaps[item.moduleId].selectIds.delete(item.id);
innerSelectedModulesMaps[item.moduleId].excludeIds.add(item.id);
setSelectedModuleStatus(moduleId);
updateSelectModule('all', item.id);
});
}
}
function setModuleTree(tree: MsTreeNodeData[]) {
moduleTree.value = tree;
}
function clearSelector() {
Object.keys(innerSelectedModulesMaps).forEach((key) => {
delete innerSelectedModulesMaps[key];
});
}
watch(
() => propsRes.data,
async () => {
await initPropsDataSort();
initTableDataSelected();
},
{
deep: true,
immediate: true,
}
);
watch(
() => innerSelectedModulesMaps,
() => {
initTableDataSelected();
},
{
deep: true,
}
);
return {
moduleSelectedMap,
rowSelectChange,
selectAllChange,
initTableDataSelected,
initPropsDataSort,
setUnSelectNode,
setSelectedModuleStatus,
setSelectedAll,
updateSelectModule,
clearSelector,
setModuleTree,
};
}

View File

@ -12,7 +12,7 @@
:extra-modules-params="{ :extra-modules-params="{
testPlanId: props?.testPlanId, testPlanId: props?.testPlanId,
}" }"
:associated-ids="props.hasNotAssociatedIds || []" :modules-maps="props.modulesMaps"
:associated-type="associationType" :associated-type="associationType"
@save="saveHandler" @save="saveHandler"
> >
@ -24,18 +24,19 @@
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
import MsCaseAssociate from '@/components/business/ms-associate-case/index.vue'; import MsCaseAssociate from '@/components/business/ms-associate-case/index.vue';
import type { saveParams } from '@/components/business/ms-associate-case/types';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
import type { AssociateCaseRequest, AssociateCaseRequestType } from '@/models/testPlan/testPlan'; import type { AssociateCaseRequestParams, AssociateCaseRequestType } from '@/models/testPlan/testPlan';
import { CaseCountApiTypeEnum, CaseModulesApiTypeEnum, CasePageApiTypeEnum } from '@/enums/associateCaseEnum'; import { CaseCountApiTypeEnum, CaseModulesApiTypeEnum, CasePageApiTypeEnum } from '@/enums/associateCaseEnum';
import { CaseLinkEnum } from '@/enums/caseEnum'; import { CaseLinkEnum } from '@/enums/caseEnum';
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps<{ const props = defineProps<{
associationType: CaseLinkEnum; associationType: CaseLinkEnum;
hasNotAssociatedIds?: string[]; modulesMaps?: Record<string, saveParams>;
saveApi?: (params: AssociateCaseRequestType) => Promise<any>; saveApi?: (params: AssociateCaseRequestType) => Promise<any>;
testPlanId?: string; testPlanId?: string;
}>(); }>();
@ -43,7 +44,7 @@
required: true, required: true,
}); });
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'success', val: AssociateCaseRequest): void; (e: 'success', val: AssociateCaseRequestParams): void;
}>(); }>();
const appStore = useAppStore(); const appStore = useAppStore();
@ -53,7 +54,7 @@
const confirmLoading = ref<boolean>(false); const confirmLoading = ref<boolean>(false);
const planId = ref(route.query.id as string); const planId = ref(route.query.id as string);
async function saveHandler(params: AssociateCaseRequest) { async function saveHandler(params: AssociateCaseRequestParams) {
if (typeof props.saveApi !== 'function') { if (typeof props.saveApi !== 'function') {
emit('success', { ...params }); emit('success', { ...params });
} else { } else {

View File

@ -105,10 +105,7 @@
<a-divider margin="4px" direction="vertical" /> <a-divider margin="4px" direction="vertical" />
<MsButton <MsButton
type="text" type="text"
:disabled=" :disabled="!hasEditPermission || selectedAssociateCasesParams.totalCount === 0"
!hasEditPermission ||
(selectedAssociateCasesParams.totalCount || selectedAssociateCasesParams.selectIds.length) === 0
"
@click="clearSelectedCases" @click="clearSelectedCases"
> >
{{ t('caseManagement.caseReview.clearSelectedCases') }} {{ t('caseManagement.caseReview.clearSelectedCases') }}
@ -120,9 +117,7 @@
<div class="text-[var(--color-text-2)]"> <div class="text-[var(--color-text-2)]">
{{ {{
t('ms.minders.selectedCases', { t('ms.minders.selectedCases', {
count: selectedAssociateCasesParams.selectAll count: selectedAssociateCasesParams.totalCount,
? selectedAssociateCasesParams.totalCount
: selectedAssociateCasesParams.selectIds.length,
}) })
}} }}
</div> </div>
@ -243,8 +238,8 @@
<caseAssociate <caseAssociate
v-model:visible="caseAssociateVisible" v-model:visible="caseAssociateVisible"
:association-type="currentSelectCase" :association-type="currentSelectCase"
:has-not-associated-ids="selectedAssociateCasesParams.selectIds"
:test-plan-id="props.planId" :test-plan-id="props.planId"
:modules-maps="selectedAssociateCasesParams.moduleMaps"
@success="writeAssociateCases" @success="writeAssociateCases"
/> />
</template> </template>
@ -273,7 +268,7 @@
import { hasAnyPermission } from '@/utils/permission'; import { hasAnyPermission } from '@/utils/permission';
import { import {
AssociateCaseRequest, AssociateCaseRequestParams,
PlanMinderEditListItem, PlanMinderEditListItem,
PlanMinderNode, PlanMinderNode,
PlanMinderNodeData, PlanMinderNodeData,
@ -569,7 +564,7 @@
const caseAssociateVisible = ref<boolean>(false); const caseAssociateVisible = ref<boolean>(false);
// //
const selectedAssociateCasesParams = ref<AssociateCaseRequest>({ const selectedAssociateCasesParams = ref<AssociateCaseRequestParams>({
excludeIds: [], excludeIds: [],
selectIds: [], selectIds: [],
selectAll: false, selectAll: false,
@ -578,26 +573,30 @@
versionId: '', versionId: '',
refId: '', refId: '',
projectId: '', projectId: '',
moduleMaps: {},
syncCase: true,
apiCaseCollectionId: '',
apiScenarioCollectionId: '',
selectAllModule: false,
}); });
function writeAssociateCases(param: AssociateCaseRequest) { function writeAssociateCases(param: AssociateCaseRequestParams) {
selectedAssociateCasesParams.value = { ...param }; selectedAssociateCasesParams.value = { ...param };
const node: PlanMinderNode = window.minder.getSelectedNode(); const node: PlanMinderNode = window.minder.getSelectedNode();
let associateType: string = ''; let associateType: string = '';
if (node.data.type === PlanMinderCollectionType.SCENARIO) { if (node.data.type === PlanMinderCollectionType.SCENARIO) {
associateType = PlanMinderAssociateType.SCENARIO_CASE; associateType = PlanMinderAssociateType.SCENARIO_CASE;
} else { } else {
associateType = associateType = param?.associateType ?? node.data.type;
node.data.type === PlanMinderCollectionType.API && param.associateApiType
? param.associateApiType
: node.data.type;
} }
node.data.associateDTOS = [ node.data.associateDTOS = [
{ {
ids: param.selectIds, ...cloneDeep(param),
associateType, associateType,
}, },
]; ];
caseAssociateVisible.value = false; caseAssociateVisible.value = false;
} }
@ -611,6 +610,11 @@
versionId: '', versionId: '',
refId: '', refId: '',
projectId: '', projectId: '',
moduleMaps: {},
syncCase: true,
apiCaseCollectionId: '',
apiScenarioCollectionId: '',
selectAllModule: false,
}; };
const node: PlanMinderNode = window.minder.getNodeById(activePlanSet.value?.data.id); const node: PlanMinderNode = window.minder.getNodeById(activePlanSet.value?.data.id);
if (node?.data) { if (node?.data) {

View File

@ -6,6 +6,7 @@
ref="treeRef" ref="treeRef"
v-model:selected-keys="selectedKeys" v-model:selected-keys="selectedKeys"
v-model:checked-keys="checkedKeys" v-model:checked-keys="checkedKeys"
v-model:half-checked-keys="halfCheckedKeys"
:data="filterTreeData" :data="filterTreeData"
class="ms-tree" class="ms-tree"
:allow-drop="handleAllowDrop" :allow-drop="handleAllowDrop"
@ -119,6 +120,7 @@
emptyText?: string; // emptyText?: string; //
checkable?: boolean; // checkable?: boolean; //
checkedStrategy?: 'all' | 'parent' | 'child'; // checkedStrategy?: 'all' | 'parent' | 'child'; //
checkStrictly?: boolean; //
virtualListProps?: VirtualListProps; // virtualListProps?: VirtualListProps; //
disabledTitleTooltip?: boolean; // tooltip disabledTitleTooltip?: boolean; // tooltip
actionOnNodeClick?: 'expand'; // actionOnNodeClick?: 'expand'; //
@ -167,7 +169,7 @@
): void; ): void;
(e: 'moreActionSelect', item: ActionsItem, node: MsTreeNodeData): void; (e: 'moreActionSelect', item: ActionsItem, node: MsTreeNodeData): void;
(e: 'moreActionsClose'): void; (e: 'moreActionsClose'): void;
(e: 'check', val: Array<string | number>): void; (e: 'check', val: Array<string | number>, node: MsTreeNodeData): void;
(e: 'expand', node: MsTreeExpandedData): void; (e: 'expand', node: MsTreeExpandedData): void;
}>(); }>();
@ -180,6 +182,9 @@
const checkedKeys = defineModel<(string | number)[]>('checkedKeys', { const checkedKeys = defineModel<(string | number)[]>('checkedKeys', {
default: [], default: [],
}); });
const halfCheckedKeys = defineModel<(string | number)[]>('halfCheckedKeys', {
default: [],
});
const focusNodeKey = defineModel<string | number>('focusNodeKey', { const focusNodeKey = defineModel<string | number>('focusNodeKey', {
default: '', default: '',
}); });
@ -362,8 +367,8 @@
emit('select', _selectedKeys, selectNode); emit('select', _selectedKeys, selectNode);
} }
function checked(_checkedKeys: Array<string | number>) { function checked(_checkedKeys: Array<string | number>, checkedNodes: MsTreeNodeData) {
emit('check', _checkedKeys); emit('check', _checkedKeys, checkedNodes);
} }
const focusEl = ref<HTMLElement | null>(); // const focusEl = ref<HTMLElement | null>(); //

View File

@ -250,7 +250,9 @@
:class="{ 'justify-between': showBatchAction }" :class="{ 'justify-between': showBatchAction }"
> >
<span v-if="props.actionConfig && selectedCount > 0 && !showBatchAction" class="title text-[var(--color-text-2)]"> <span v-if="props.actionConfig && selectedCount > 0 && !showBatchAction" class="title text-[var(--color-text-2)]">
<slot name="count">
{{ t('msTable.batch.selected', { count: selectedCount }) }} {{ t('msTable.batch.selected', { count: selectedCount }) }}
</slot>
<a-button class="clear-btn ml-[12px] px-2" type="text" @click="emit('clearSelector')"> <a-button class="clear-btn ml-[12px] px-2" type="text" @click="emit('clearSelector')">
{{ t('msTable.batch.clear') }} {{ t('msTable.batch.clear') }}
</a-button> </a-button>
@ -265,7 +267,11 @@
:size="props.paginationSize" :size="props.paginationSize"
@batch-action="handleBatchAction" @batch-action="handleBatchAction"
@clear="() => emit('clearSelector')" @clear="() => emit('clearSelector')"
/> >
<template #count>
<slot name="count"></slot>
</template>
</batch-action>
</div> </div>
<ms-pagination <ms-pagination
v-if="!!attrs.showPagination" v-if="!!attrs.showPagination"

View File

@ -1,6 +1,8 @@
<template> <template>
<div v-if="props.actionConfig" ref="refWrapper" class="flex flex-row flex-nowrap items-center"> <div v-if="props.actionConfig" ref="refWrapper" class="flex flex-row flex-nowrap items-center">
<slot name="count">
<div class="title one-line-text">{{ t('msTable.batch.selected', { count: props.selectRowCount }) }}</div> <div class="title one-line-text">{{ t('msTable.batch.selected', { count: props.selectRowCount }) }}</div>
</slot>
<template v-for="(element, idx) in baseAction" :key="element.label"> <template v-for="(element, idx) in baseAction" :key="element.label">
<a-divider v-if="element.isDivider" class="divider mx-0 my-[6px]" /> <a-divider v-if="element.isDivider" class="divider mx-0 my-[6px]" />
<a-button <a-button

View File

@ -1,5 +1,6 @@
import type { MinderJsonNode, MinderJsonNodeData } from '@/components/pure/ms-minder-editor/props'; import type { MinderJsonNode, MinderJsonNodeData } from '@/components/pure/ms-minder-editor/props';
import type { BatchActionQueryParams } from '@/components/pure/ms-table/type'; import type { BatchActionQueryParams } from '@/components/pure/ms-table/type';
import type { saveParams } from '@/components/business/ms-associate-case/types';
import type { customFieldsItem } from '@/models/caseManagement/featureCase'; import type { customFieldsItem } from '@/models/caseManagement/featureCase';
import type { TableQueryParams } from '@/models/common'; import type { TableQueryParams } from '@/models/common';
@ -39,6 +40,16 @@ export interface AssociateCaseRequest extends BatchApiParams {
associateApiType?: string; associateApiType?: string;
} }
export interface AssociateCaseRequestParams extends AssociateCaseRequest {
associateType?: string;
moduleMaps?: Record<string, saveParams>;
syncCase: boolean;
apiCaseCollectionId: string;
apiScenarioCollectionId: string;
selectAllModule: boolean;
projectId: string;
}
export type AssociateCaseRequestType = Pick<AssociateCaseRequest, 'functionalSelectIds' | 'testPlanId'>; export type AssociateCaseRequestType = Pick<AssociateCaseRequest, 'functionalSelectIds' | 'testPlanId'>;
export interface AddTestPlanParams { export interface AddTestPlanParams {