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

View File

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

View File

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

View File

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

View File

@ -28,10 +28,21 @@
</a-button>
</a-tooltip>
</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
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"
:empty-text="t('common.noData')"
:virtual-list-props="virtualListProps"
@ -44,14 +55,30 @@
:expand-all="isExpandAll"
block-node
title-tooltip-position="top"
checkable
check-strictly
@select="folderNodeSelect"
@check="checkNode"
>
<template #title="nodeData">
<div class="inline-flex w-full gap-[8px]">
<div class="one-line-text w-full text-[var(--color-text-1)]">{{ nodeData.name }}</div>
<div class="ms-tree-node-count ml-[4px] text-[var(--color-text-brand)]">{{ nodeData.count || 0 }}</div>
<div class="ms-tree-node-count ml-[4px] flex items-center text-[var(--color-text-brand)]">
{{ nodeData.count || 0 }}
</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>
</MsTree>
</a-spin>
</template>
@ -60,6 +87,7 @@
import { ref } from 'vue';
import { useVModel } from '@vueuse/core';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsTree from '@/components/business/ms-tree/index.vue';
import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
import TreeFolderAll from '@/views/api-test/components/treeFolderAll.vue';
@ -76,7 +104,7 @@
const { t } = useI18n();
const props = defineProps<{
modulesCount?: Record<string, number>; //
modulesCount: Record<string, number>; //
selectedKeys: string[]; // key
currentProject: string;
getModulesApiType: CaseModulesApiTypeEnum[keyof CaseModulesApiTypeEnum];
@ -91,9 +119,25 @@
(e: 'init', params: ModuleTreeNode[], selectedProtocols?: string[]): void;
(e: 'changeProtocol', selectedProtocols: 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 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 activeFolder = ref<string>('all');
const allCount = ref(0);
@ -103,7 +147,7 @@
const virtualListProps = computed(() => {
return {
height: 'calc(100vh - 180px)',
height: 'calc(100vh - 236px)',
threshold: 200,
fixedSize: true,
buffer: 15, // 10 padding
@ -129,6 +173,42 @@
}
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);
caseTree.value = mapTree<ModuleTreeNode>(res, (node) => {
return {
...node,
count: props.modulesCount?.[node.id] || 0,
};
});
calculateTreeCount(res);
if (setDefault) {
setActiveFolder('all');
}
emit('init', caseTree.value, selectedProtocols.value);
} catch (error) {
// eslint-disable-next-line no-console
@ -166,22 +244,33 @@
initModules();
}
function checkNode(_checkedKeys: Array<string | number>, checkedNodes: MsTreeNodeData) {
emit('check', _checkedKeys, checkedNodes);
}
function selectCurrent(node: MsTreeNodeData, isSelected: boolean) {
emit('selectParent', node, isSelected);
}
/**
* 初始化模块文件数量
*/
watch(
() => props.modulesCount,
(obj) => {
caseTree.value = mapTree<ModuleTreeNode>(caseTree.value, (node) => {
return {
...node,
count: obj?.[node.id] || 0,
};
});
allCount.value = obj?.all || 0;
() => {
calculateTreeCount(caseTree.value);
},
{
deep: true,
immediate: true,
}
);
function handleChangeAll(value: boolean | (string | number | boolean)[], ev: Event) {
isCheckAll.value = value as boolean;
emit('checkAllModule', isCheckAll.value);
}
watch(
() => props.showType,
(val) => {

View File

@ -113,11 +113,15 @@
</a-input-group>
</div>
</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]">
<CaseTree
ref="caseTreeRef"
v-model:checkedKeys="checkedKeys"
v-model:selected-keys="selectedKeys"
v-model:halfCheckedKeys="halfCheckedKeys"
v-model:isCheckedAll="isCheckedAll"
v-model:indeterminate="indeterminate"
:modules-count="modulesCount"
:get-modules-api-type="props.getModulesApiType"
:current-project="innerProject"
@ -128,9 +132,12 @@
@folder-node-select="handleFolderNodeSelect"
@init="initModuleTree"
@change-protocol="handleProtocolChange"
@select-parent="selectParent"
@check="checkNode"
@check-all-module="checkAllModule"
/>
</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
v-model:keyword="keyword"
:filter-config-list="[]"
@ -166,18 +173,6 @@
</div>
</template>
</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>
</template>
</MsAdvanceFilter>
@ -186,6 +181,7 @@
v-if="associationType === CaseLinkEnum.FUNCTIONAL"
ref="functionalTableRef"
v-model:selectedIds="selectedIds"
v-model:selectedModulesMaps="selectedModulesMaps"
:association-type="associateType"
:get-page-api-type="getPageApiType"
:active-module="activeFolder"
@ -195,14 +191,18 @@
:active-source-type="associationType"
:extra-table-params="props.extraTableParams"
:keyword="keyword"
:module-tree="moduleTree"
@get-module-count="initModulesCount"
@refresh="loadCaseList"
/>
>
<TotalCount :total-count="totalCount" />
</CaseTable>
<!-- 接口用例 API -->
<ApiTable
v-if="associationType === CaseLinkEnum.API && showType === 'API'"
ref="apiTableRef"
v-model:selectedIds="selectedIds"
v-model:selectedModulesMaps="selectedModulesMaps"
:get-page-api-type="getPageApiType"
:extra-table-params="props.extraTableParams"
:association-type="associateType"
@ -214,13 +214,17 @@
:keyword="keyword"
:show-type="showType"
:protocols="selectedProtocols"
:module-tree="moduleTree"
@get-module-count="initModulesCount"
/>
>
<TotalCount :total-count="totalCount" />
</ApiTable>
<!-- 接口用例 CASE -->
<ApiCaseTable
v-if="associationType === CaseLinkEnum.API && showType === 'CASE'"
ref="caseTableRef"
v-model:selectedIds="selectedIds"
v-model:selectedModulesMaps="selectedModulesMaps"
:get-page-api-type="getPageApiType"
:extra-table-params="props.extraTableParams"
:association-type="associateType"
@ -232,12 +236,16 @@
:keyword="keyword"
:show-type="showType"
:protocols="selectedProtocols"
:module-tree="moduleTree"
@get-module-count="initModulesCount"
/>
>
<TotalCount :total-count="totalCount" />
</ApiCaseTable>
<!-- 接口场景用例 -->
<ScenarioCaseTable
v-if="associationType === CaseLinkEnum.SCENARIO"
ref="scenarioTableRef"
v-model:selectedModulesMaps="selectedModulesMaps"
v-model:selectedIds="selectedIds"
:association-type="associateType"
:modules-count="modulesCount"
@ -249,30 +257,85 @@
:get-page-api-type="getPageApiType"
:extra-table-params="props.extraTableParams"
:keyword="keyword"
:module-tree="moduleTree"
:total-count="totalCount"
@get-module-count="initModulesCount"
@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">
<div class="flex flex-1 items-center">
<slot name="footerLeft"></slot>
<a-tree-select
v-model="apiScenarioCollectionId"
: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>
<div class="flex items-center">
<slot name="footerRight">
<a-button type="secondary" :disabled="props.confirmLoading" class="mr-[12px]" @click="cancel">
{{ t('common.cancel') }}
</a-button>
<a-button
:loading="props.confirmLoading"
type="primary"
:disabled="!selectedIds.length"
@click="handleConfirm"
>
{{ t('ms.case.associate.associate') }}
</a-button>
</slot>
</div>
</div>
</slot>
</div>
<div class="flex items-center">
<slot name="footerRight">
<a-button type="secondary" :disabled="props.confirmLoading" class="mr-[12px]" @click="cancel">
{{ t('common.cancel') }}
</a-button>
<a-button
:loading="props.confirmLoading"
type="primary"
:disabled="!isDisabledSaveButton"
@click="handleConfirm"
>
{{ t('ms.case.associate.associate') }}
</a-button>
</slot>
</div>
</div>
</MsDrawer>
@ -284,13 +347,16 @@
import { MsAdvanceFilter } from '@/components/pure/ms-advance-filter';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
import ApiCaseTable from './apiCaseTable.vue';
import ApiTable from './apiTable.vue';
import CaseTable from './caseTable.vue';
import CaseTree from './caseTree.vue';
import ScenarioCaseTable from './scenarioCaseTable.vue';
import TotalCount from './totalCount.vue';
import { getAssociatedProjectOptions } from '@/api/modules/case-management/featureCase';
import { getApiCaseModule, getApiScenarioModule } from '@/api/modules/test-plan/testPlan';
import { useI18n } from '@/hooks/useI18n';
import useVisit from '@/hooks/useVisit';
import useAppStore from '@/store/modules/app';
@ -300,10 +366,10 @@
import { CaseModulesApiTypeEnum, CasePageApiTypeEnum } from '@/enums/associateCaseEnum';
import { CaseLinkEnum } from '@/enums/caseEnum';
import type { moduleKeysType, saveParams } from './types';
import { initGetModuleCountFunc } from './utils/moduleCount';
const visitedKey = 'changeLinkProject';
const { addVisited, getIsVisited } = useVisit(visitedKey);
const appStore = useAppStore();
@ -321,6 +387,7 @@
extraModuleCountParams?: TableQueryParams; //
okButtonDisabled?: boolean; //
confirmLoading?: boolean;
modulesMaps?: Record<string, saveParams>;
associatedIds?: string[]; // id
hideProjectSelect?: boolean; //
associatedType: keyof typeof CaseLinkEnum; //
@ -351,11 +418,24 @@
const activeFolder = ref('all');
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({
get: () => [activeFolder.value],
set: (val) => val,
});
const syncCase = ref<boolean>(true);
const folderName = computed(() => {
switch (associationType.value) {
case CaseLinkEnum.FUNCTIONAL:
@ -399,26 +479,54 @@
const caseTableRef = ref<InstanceType<typeof ApiCaseTable>>();
const scenarioTableRef = ref<InstanceType<typeof ScenarioCaseTable>>();
function makeParams() {
switch (props.associatedType) {
case CaseLinkEnum.FUNCTIONAL:
return functionalTableRef.value?.getFunctionalSaveParams();
case CaseLinkEnum.API:
return showType.value === 'API'
? apiTableRef.value?.getApiSaveParams()
: caseTableRef.value?.getApiCaseSaveParams();
case CaseLinkEnum.SCENARIO:
return scenarioTableRef.value?.getScenarioSaveParams();
default:
break;
}
function getMapParams() {
const selectedParams: Record<string, saveParams> = {};
Object.entries(selectedModulesMaps.value).forEach(([moduleId, selectedProps]) => {
const { selectAll, selectIds, excludeIds, count } = selectedProps;
selectedParams[moduleId] = {
count,
selectAll,
selectIds: [...selectIds],
excludeIds: [...excludeIds],
};
});
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() {
const params = makeParams();
if (!params?.selectIds.length) {
return;
const params = {
moduleMaps: getMapParams(),
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);
}
@ -445,11 +553,15 @@
console.log(error);
}
}
const selectedProtocols = ref<string[]>([]);
async function initModulesCount(params: TableQueryParams) {
try {
modulesCount.value = await initGetModuleCountFunc(props.getModuleCountApiType, associationType.value, {
...params,
moduleIds: [],
filter: {},
keyword: '',
...props.extraModuleCountParams,
protocols: associationType.value === CaseLinkEnum.API ? selectedProtocols.value : undefined,
});
@ -480,20 +592,32 @@
const moduleTree = ref<ModuleTreeNode[]>([]);
function initModuleTree(tree: ModuleTreeNode[], _protocols?: string[]) {
moduleTree.value = unref(tree);
moduleTree.value = tree;
selectedProtocols.value = _protocols || [];
if (props.associatedType === CaseLinkEnum.API) {
loadCaseList();
}
}
const functionalType = ref('project');
const functionalList = ref([
{
id: 'project',
name: t('ms.case.associate.project'),
},
]);
const apiSetTree = ref<ModuleTreeNode[]>();
const scenarioSetTree = ref<ModuleTreeNode[]>();
async function initTestSet() {
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) {
if (visible && !getIsVisited()) {
@ -527,6 +651,7 @@
associationType.value = props.associatedType;
activeFolder.value = 'all';
initProjectList();
initTestSet();
}
selectPopVisible.value = false;
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>
<style scoped lang="less">

View File

@ -22,4 +22,14 @@ export default {
'ms.case.associate.switchProject': 'Switch project?',
'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.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.switchProjectPopTip': '切换后,已选数据将清空',
'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"
@filter-change="getModuleCount"
@row-select-change="rowSelectChange"
@select-all-change="selectAllChange"
@clear-selector="clearSelector"
>
<template #num="{ record }">
<MsButton type="text" @click="toDetail(record)">{{ record.num }}</MsButton>
@ -34,6 +37,9 @@
<div class="one-line-text">{{ record.createUserName }}</div>
</a-tooltip>
</template>
<template #count>
<slot></slot>
</template>
</MsBaseTable>
</template>
@ -45,6 +51,7 @@
import { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import CaseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
import ExecutionStatus from '@/views/api-test/report/component/reportStatus.vue';
import { useI18n } from '@/hooks/useI18n';
@ -61,6 +68,8 @@
import { SpecialColumnEnum, TableKeyEnum } from '@/enums/tableEnum';
import { FilterRemoteMethodsEnum, FilterSlotNameEnum } from '@/enums/tableFilterEnum';
import type { moduleKeysType } from './types';
import useModuleSelection from './useModuleSelection';
import { getPublicLinkCaseListMap } from './utils/page';
import { casePriorityOptions } from '@/views/api-test/components/config';
@ -77,6 +86,7 @@
keyword: string;
getPageApiType: keyof typeof CasePageApiTypeEnum; // Api
extraTableParams?: TableQueryParams; //
moduleTree: MsTreeNodeData[];
}>();
const emit = defineEmits<{
@ -89,6 +99,10 @@
const appStore = useAppStore();
const tableStore = useTableStore();
const innerSelectedModulesMaps = defineModel<Record<string, moduleKeysType>>('selectedModulesMaps', {
required: true,
});
const statusList = computed(() => {
return Object.keys(ReportStatus).map((key) => {
return {
@ -191,8 +205,10 @@
const getPageList = computed(() => {
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,
showSetting: true,
isSimpleSetting: true,
@ -201,7 +217,8 @@
showSelectAll: true,
heightUsed: 310,
showSelectorAll: false,
});
}
);
async function getTableParams() {
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) {
openNewPage(ApiTestRouteEnum.API_TEST_SCENARIO, {
@ -273,7 +306,6 @@
});
}
watch([() => props.currentProject, () => props.activeModule], () => {
resetSelector();
resetFilterParams();
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="{
testPlanId: props?.testPlanId,
}"
:associated-ids="props.hasNotAssociatedIds || []"
:modules-maps="props.modulesMaps"
:associated-type="associationType"
@save="saveHandler"
>
@ -24,18 +24,19 @@
import { Message } from '@arco-design/web-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 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 { CaseLinkEnum } from '@/enums/caseEnum';
const { t } = useI18n();
const props = defineProps<{
associationType: CaseLinkEnum;
hasNotAssociatedIds?: string[];
modulesMaps?: Record<string, saveParams>;
saveApi?: (params: AssociateCaseRequestType) => Promise<any>;
testPlanId?: string;
}>();
@ -43,7 +44,7 @@
required: true,
});
const emit = defineEmits<{
(e: 'success', val: AssociateCaseRequest): void;
(e: 'success', val: AssociateCaseRequestParams): void;
}>();
const appStore = useAppStore();
@ -53,7 +54,7 @@
const confirmLoading = ref<boolean>(false);
const planId = ref(route.query.id as string);
async function saveHandler(params: AssociateCaseRequest) {
async function saveHandler(params: AssociateCaseRequestParams) {
if (typeof props.saveApi !== 'function') {
emit('success', { ...params });
} else {

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import type { MinderJsonNode, MinderJsonNodeData } from '@/components/pure/ms-minder-editor/props';
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 { TableQueryParams } from '@/models/common';
@ -39,6 +40,16 @@ export interface AssociateCaseRequest extends BatchApiParams {
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 interface AddTestPlanParams {