feat(接口管理): 用例编辑&重构用例详情

This commit is contained in:
teukkk 2024-03-17 17:29:19 +08:00 committed by Craftsman
parent f5dc90ffd8
commit 88a4e8b1e3
15 changed files with 624 additions and 241 deletions

View File

@ -55,6 +55,7 @@ import {
SortCaseUrl,
SortDefinitionUrl,
SwitchDefinitionScheduleUrl,
ToggleFollowCaseUrl,
ToggleFollowDefinitionUrl,
TransferFileCaseUrl,
TransferFileModuleOptionCaseUrl,
@ -401,6 +402,11 @@ export function getCaseDetail(id: string) {
return MSR.get<ApiCaseDetail>({ url: GetCaseDetailUrl, params: id });
}
// 关注/取消关注接口用例
export function toggleFollowCase(id: string | number) {
return MSR.get({ url: ToggleFollowCaseUrl, params: id });
}
/**
*
*/

View File

@ -73,6 +73,7 @@ export const ExecuteCaseUrl = '/api/case/run/'; // 单独执行接口用例
export const GetExecuteHistoryUrl = 'api/case/execute/page'; // 获取用的执行历史
export const GetDependencyUrl = '/api/case/get-reference'; // 获取用例的依赖关系
export const GetChangeHistoryUrl = '/api/case/operation-history/page'; // 获取用例的依赖关系
export const ToggleFollowCaseUrl = '/api/case/follow'; // 接口定义-关注/取消关注
/**
*

View File

@ -373,7 +373,7 @@ export interface ExecutePluginRequestParams {
// 执行接口调试入参
export interface ExecuteRequestParams {
id?: string;
reportId: string;
reportId?: string;
environmentId: string;
uploadFileIds: string[];
linkFileIds: string[];

View File

@ -343,8 +343,10 @@ export interface AddApiCaseParams extends ExecuteRequestParams {
name: string;
priority: string;
status: string;
apiDefinitionId: string | number;
apiDefinitionId?: string | number;
tags: string[];
deleteFileIds?: string[];
unLinkFileIds?: string[];
}
export interface ApiRunModeRequest {

View File

@ -139,7 +139,7 @@
</div>
</div>
</div>
<div class="request-params-tab px-[16px]">
<div class="px-[16px]">
<MsTab
v-model:active-key="requestVModel.activeTab"
:content-tab-list="contentTabList"
@ -1528,9 +1528,6 @@
:deep(.arco-tabs-tab) {
@apply leading-none;
}
.request-params-tab :deep(.arco-tabs-nav-tab) {
border-bottom: 1px solid var(--color-text-n8) !important;
}
.hidden-second {
:deep(.arco-split-trigger) {
@apply hidden;

View File

@ -338,6 +338,7 @@
import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
const props = defineProps<{
isCase?: boolean; // case
detail: RequestParam;
protocols: ProtocolItem[];
}>();
@ -409,6 +410,12 @@
}
}
watchEffect(() => {
if (!props.isCase) return;
// case
previewDetail.value = cloneDeep(props.detail); // props.detailprops.detail
});
watch(
() => props.detail.id,
() => {

View File

@ -2,19 +2,6 @@
<div class="h-full w-full overflow-hidden">
<div class="px-[18px] pt-[16px]">
<MsDetailCard
v-if="props.isCaseDetail"
:title="`【${previewDetail.num}】${previewDetail.name}`"
:description="description"
>
<template #type="{ value }">
<apiMethodName :method="value as RequestMethods" tag-size="small" is-tag />
</template>
<template #priority="{ value }">
<caseLevel :case-level="value as CaseLevel" />
</template>
</MsDetailCard>
<MsDetailCard
v-else
:title="`【${previewDetail.num}】${previewDetail.name}`"
:description="description"
:simple-show-count="4"
@ -78,8 +65,6 @@
import MsDetailCard from '@/components/pure/ms-detail-card/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
import type { CaseLevel } from '@/components/business/ms-case-associate/types';
import detailTab from './detail.vue';
import history from './history.vue';
import quote from './quote.vue';
@ -100,7 +85,6 @@
detail: RequestParam;
moduleTree: ModuleTreeNode[];
protocols: ProtocolItem[];
isCaseDetail?: boolean; //
}>();
const emit = defineEmits(['updateFollow']);
@ -113,7 +97,6 @@
() => props.detail.id,
() => {
previewDetail.value = cloneDeep(props.detail); // props.detailprops.detail
if (props.isCaseDetail) return;
const tableParam = getValidRequestTableParams(previewDetail.value); // props.detail
previewDetail.value = {
...previewDetail.value,
@ -137,66 +120,49 @@
}
);
const description = computed(() => {
const commonDescription = [
{
key: 'type',
locale: 'apiTestManagement.apiType',
value: previewDetail.value.method,
},
{
key: 'path',
locale: 'apiTestManagement.path',
value: previewDetail.value.url || previewDetail.value.path,
},
{
key: 'tags',
locale: 'common.tag',
value: previewDetail.value.tags,
},
];
if (!props.isCaseDetail) {
return [
...commonDescription,
...[
{
key: 'description',
locale: 'common.desc',
value: previewDetail.value.description,
width: '100%',
},
{
key: 'belongModule',
locale: 'apiTestManagement.belongModule',
value: findNodeByKey<ModuleTreeNode>(props.moduleTree, previewDetail.value.moduleId, 'id')?.path,
},
{
key: 'creator',
locale: 'common.creator',
value: previewDetail.value.createUserName,
},
{
key: 'createTime',
locale: 'apiTestManagement.createTime',
value: dayjs(previewDetail.value.createTime).format('YYYY-MM-DD HH:mm:ss'),
},
{
key: 'updateTime',
locale: 'apiTestManagement.updateTime',
value: dayjs(previewDetail.value.updateTime).format('YYYY-MM-DD HH:mm:ss'),
},
],
];
}
//
const caseDescription = commonDescription.slice();
caseDescription.splice(1, 0, {
key: 'priority',
locale: 'case.caseLevel',
value: previewDetail.value.priority,
});
return caseDescription;
});
const description = computed(() => [
{
key: 'type',
locale: 'apiTestManagement.apiType',
value: previewDetail.value.method,
},
{
key: 'path',
locale: 'apiTestManagement.path',
value: previewDetail.value.url || previewDetail.value.path,
},
{
key: 'tags',
locale: 'common.tag',
value: previewDetail.value.tags,
},
{
key: 'description',
locale: 'common.desc',
value: previewDetail.value.description,
width: '100%',
},
{
key: 'belongModule',
locale: 'apiTestManagement.belongModule',
value: findNodeByKey<ModuleTreeNode>(props.moduleTree, previewDetail.value.moduleId, 'id')?.path,
},
{
key: 'creator',
locale: 'common.creator',
value: previewDetail.value.createUserName,
},
{
key: 'createTime',
locale: 'apiTestManagement.createTime',
value: dayjs(previewDetail.value.createTime).format('YYYY-MM-DD HH:mm:ss'),
},
{
key: 'updateTime',
locale: 'apiTestManagement.updateTime',
value: dayjs(previewDetail.value.updateTime).format('YYYY-MM-DD HH:mm:ss'),
},
]);
const followLoading = ref(false);
async function toggleFollowReview() {

View File

@ -1,33 +1,126 @@
<template>
<preview
:detail="activeApiTab"
:module-tree="props.moduleTree"
:protocols="protocols"
is-case-detail
@update-follow="activeApiTab.follow = !activeApiTab.follow"
/>
<div class="h-full w-full overflow-hidden">
<a-tabs v-model:active-key="activeKey" class="h-full px-[16px]" animation lazy-load>
<template #extra>
<div v-show="!props.isDrawer" class="flex gap-[12px]">
<a-button type="primary">
{{ t('apiTestManagement.execute') }}
</a-button>
<a-dropdown position="br" :hide-on-select="false" @select="handleSelect">
<a-button>{{ t('common.operation') }}</a-button>
<template #content>
<a-doption value="edit">
<MsIcon type="icon-icon_edit_outlined" />
{{ t('common.edit') }}
</a-doption>
<a-doption value="share">
<MsIcon type="icon-icon_share1" />
{{ t('common.share') }}
</a-doption>
<a-doption value="fork">
<MsIcon
:type="caseDetail.follow ? 'icon-icon_collect_filled' : 'icon-icon_collection_outlined'"
:class="`${caseDetail.follow ? 'text-[rgb(var(--warning-6))]' : ''}`"
/>
{{ t('common.fork') }}
</a-doption>
<a-divider margin="4px" />
<a-doption class="error-6 text-[rgb(var(--danger-6))]">
<MsIcon type="icon-icon_delete-trash_outlined" class="text-[rgb(var(--danger-6))]" />
{{ t('common.delete') }}
</a-doption>
</template>
</a-dropdown>
</div>
</template>
<a-tab-pane key="detail" :title="t('apiTestManagement.detail')" class="px-[18px] py-[16px]">
<MsDetailCard :title="`【${caseDetail.num}】${caseDetail.name}`" :description="description" class="mb-[8px]">
<template #titleAppend>
<a-button v-show="props.isDrawer" type="primary" size="mini">
{{ t('apiTestManagement.execute') }}
</a-button>
</template>
<template #type="{ value }">
<apiMethodName :method="value as RequestMethods" tag-size="small" is-tag />
</template>
<template #priority="{ value }">
<caseLevel :case-level="value as CaseLevel" />
</template>
</MsDetailCard>
<detailTab :detail="caseDetail" :protocols="protocols" is-case />
</a-tab-pane>
<a-tab-pane key="reference" :title="t('apiTestManagement.reference')" class="px-[18px] py-[16px]">
<quote :source-id="caseDetail.id" />
</a-tab-pane>
<!-- <a-tab-pane key="dependencies" :title="t('apiTestManagement.dependencies')" class="px-[18px] py-[16px]">
</a-tab-pane> -->
<a-tab-pane key="changeHistory" :title="t('apiTestManagement.changeHistory')" class="px-[18px] py-[16px]">
<history :source-id="caseDetail.id" />
</a-tab-pane>
</a-tabs>
</div>
<createAndEditCaseDrawer ref="createAndEditCaseDrawerRef" :protocol="props.protocol" v-bind="$attrs" />
</template>
<script setup lang="ts">
import preview from '@/views/api-test/management/components/management/api/preview/index.vue';
import { useI18n } from 'vue-i18n';
import { useClipboard } from '@vueuse/core';
import { Message } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import MsDetailCard from '@/components/pure/ms-detail-card/index.vue';
import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
import type { CaseLevel } from '@/components/business/ms-case-associate/types';
import detailTab from '../api/preview/detail.vue';
import history from '../api/preview/history.vue';
import quote from '../api/preview/quote.vue';
import createAndEditCaseDrawer from './createAndEditCaseDrawer.vue';
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
import { getProtocolList } from '@/api/modules/api-test/common';
import { toggleFollowCase } from '@/api/modules/api-test/management';
import useAppStore from '@/store/modules/app';
import { ProtocolItem } from '@/models/apiTest/common';
import { ModuleTreeNode } from '@/models/common';
import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
import { RequestMethods } from '@/enums/apiEnum';
const props = defineProps<{
moduleTree: ModuleTreeNode[]; //
isDrawer?: boolean; //
detail: RequestParam;
protocol: string;
}>();
const emit = defineEmits(['updateFollow']);
const { copy, isSupported } = useClipboard();
const { t } = useI18n();
const appStore = useAppStore();
const activeApiTab = defineModel<RequestParam>('activeApiTab', {
required: true,
});
const caseDetail = computed<RequestParam>(() => cloneDeep(props.detail)); // props.detailprops.detail
const activeKey = ref('detail');
const description = computed(() => [
{
key: 'type',
locale: 'apiTestManagement.apiType',
value: caseDetail.value.method,
},
{
key: 'priority',
locale: 'case.caseLevel',
value: caseDetail.value.priority,
},
{
key: 'path',
locale: 'apiTestManagement.path',
value: caseDetail.value.url || caseDetail.value.path,
},
{
key: 'tags',
locale: 'common.tag',
value: caseDetail.value.tags,
},
]);
const protocols = ref<ProtocolItem[]>([]);
async function initProtocolList() {
@ -42,4 +135,81 @@
onBeforeMount(() => {
initProtocolList();
});
const followLoading = ref(false);
async function follow() {
try {
followLoading.value = true;
await toggleFollowCase(caseDetail.value.id);
Message.success(caseDetail.value.follow ? t('common.unFollowSuccess') : t('common.followSuccess'));
emit('updateFollow');
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
followLoading.value = false;
}
}
function share() {
if (isSupported) {
copy(`${window.location.href}&dId=${caseDetail.value.id}`);
Message.success(t('apiTestManagement.shareUrlCopied'));
} else {
Message.error(t('common.copyNotSupport'));
}
}
const createAndEditCaseDrawerRef = ref<InstanceType<typeof createAndEditCaseDrawer>>();
function editCase() {
createAndEditCaseDrawerRef.value?.open(caseDetail.value.apiDefinitionId, caseDetail.value, false);
}
function handleSelect(val: string | number | Record<string, any> | undefined) {
switch (val) {
case 'edit':
editCase();
break;
case 'share':
share();
break;
case 'fork':
follow();
break;
default:
break;
}
}
defineExpose({
editCase,
share,
follow,
});
</script>
<style lang="less" scoped>
:deep(.arco-tabs-nav) {
border-bottom: 1px solid var(--color-text-n8);
}
:deep(.arco-tabs-content) {
@apply pt-0;
.arco-tabs-content-item {
@apply px-0;
}
}
:deep(.ms-detail-card-desc) {
gap: 16px;
flex-wrap: nowrap !important;
& > div:nth-of-type(n) {
width: auto;
max-width: 30%;
}
}
.error-6 {
color: rgb(var(--danger-6));
&:hover {
color: rgb(var(--danger-6));
}
}
</style>

View File

@ -0,0 +1,103 @@
<template>
<MsDrawer
v-model:visible="innerVisible"
unmount-on-close
:title="t('caseManagement.featureCase.caseDetail')"
:width="894"
:footer="false"
no-content-padding
>
<template #headerLeft>
<environmentSelect ref="environmentSelectRef" class="ml-[16px]" />
</template>
<template #tbutton>
<div class="flex items-center gap-[4px]">
<MsButton
v-permission="['PROJECT_API_DEFINITION_CASE:READ+UPDATE']"
type="icon"
status="secondary"
@click="caseDerailRef?.editCase()"
>
<MsIcon type="icon-icon_edit_outlined" class="mr-[8px]" />
{{ t('common.edit') }}
</MsButton>
<MsButton type="icon" status="secondary" @click="caseDerailRef?.share()">
<MsIcon type="icon-icon_share1" class="mr-[8px]" />
{{ t('common.share') }}
</MsButton>
<MsButton
v-permission="['PROJECT_API_DEFINITION_CASE:READ+UPDATE']"
type="icon"
status="secondary"
@click="caseDerailRef?.follow()"
>
<MsIcon
:type="props.detail.follow ? 'icon-icon_collect_filled' : 'icon-icon_collection_outlined'"
class="mr-[8px]"
:class="[props.detail.follow ? 'text-[rgb(var(--warning-6))]' : '']"
/>
{{ t('common.fork') }}
</MsButton>
<MsButton type="icon" status="secondary">
<a-dropdown position="br">
<div>
<icon-more class="mr-[8px]" />
<span> {{ t('common.more') }}</span>
</div>
<template #content>
<a-doption
v-permission="['PROJECT_API_DEFINITION_CASE:READ+DELETE']"
class="error-6 text-[rgb(var(--danger-6))]"
>
<MsIcon type="icon-icon_delete-trash_outlined" class="text-[rgb(var(--danger-6))]" />
{{ t('common.delete') }}
</a-doption>
</template>
</a-dropdown>
</MsButton>
</div>
</template>
<caseDetail
ref="caseDerailRef"
is-drawer
:detail="props.detail"
:protocol="props.protocol"
:api-detail="props.apiDetail"
v-bind="$attrs"
/>
</MsDrawer>
</template>
<script setup lang="ts">
import MsButton from '@/components/pure/ms-button/index.vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import environmentSelect from '../../environmentSelect.vue';
import caseDetail from './caseDetail.vue';
import { useI18n } from '@/hooks/useI18n';
import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
const props = defineProps<{
detail: RequestParam;
protocol: string;
apiDetail: RequestParam;
}>();
const { t } = useI18n();
const innerVisible = defineModel<boolean>('visible', {
required: true,
});
const caseDerailRef = ref<InstanceType<typeof caseDetail>>();
</script>
<style scoped lang="less">
.error-6 {
color: rgb(var(--danger-6));
&:hover {
color: rgb(var(--danger-6));
}
}
</style>

View File

@ -1,6 +1,6 @@
<template>
<div class="overflow-hidden p-[16px_22px]">
<div class="mb-[16px] flex items-center justify-between">
<div :class="['mb-[16px]', 'flex', 'items-center', props.isApi ? 'justify-between' : 'justify-end']">
<a-button
v-show="props.isApi"
v-permission="['PROJECT_API_DEFINITION_CASE:READ+ADD']"
@ -37,7 +37,9 @@
@drag-change="handleDragChange"
>
<template #num="{ record }">
<MsButton type="text" @click="openCaseTab(record)">{{ record.num }}</MsButton>
<MsButton type="text" @click="isApi ? openCaseDetailDrawer(record.id) : openCaseTab(record)">{{
record.num
}}</MsButton>
</template>
<template #caseLevel="{ record }">
<a-select
@ -236,12 +238,19 @@
</template>
</a-modal>
<createAndEditCaseDrawer
v-if="props.isApi"
ref="createAndEditCaseDrawerRef"
:protocol="props.protocol"
:api-detail="apiDetail as RequestParam"
:api-detail="apiDetail"
@load-case="loadCaseListAndResetSelector()"
/>
<caseDetailDrawer
v-model:visible="caseDetailDrawerVisible"
:detail="caseDetail as RequestParam"
:protocol="props.protocol"
:api-detail="apiDetail as RequestParam"
@update-follow="caseDetail.follow = !caseDetail.follow"
@load-case="(id: string) => loadCase(id)"
/>
<a-modal v-model:visible="showBatchExecute" title-align="start" class="ms-modal-upload ms-modal-medium" :width="480">
<template #title>
{{ t('report.trigger.batch.execution') }}
@ -337,8 +346,10 @@
<script setup lang="ts">
import { FormInstance, Message } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import MsButton from '@/components/pure/ms-button/index.vue';
import { TabItem } from '@/components/pure/ms-editable-tab/types';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { BatchActionParams, BatchActionQueryParams, MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
@ -346,6 +357,7 @@
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
import caseDetailDrawer from './caseDetailDrawer.vue';
import createAndEditCaseDrawer from './createAndEditCaseDrawer.vue';
import apiStatus from '@/views/api-test/components/apiStatus.vue';
@ -356,6 +368,7 @@
deleteCase,
dragSort,
executeCase,
getCaseDetail,
getCasePage,
getEnvList,
getPoolId,
@ -376,6 +389,7 @@
import { TableKeyEnum } from '@/enums/tableEnum';
import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
import { parseRequestBodyFiles } from '@/views/api-test/components/utils';
const props = defineProps<{
isApi: boolean; // case tab
@ -949,15 +963,52 @@
const createAndEditCaseDrawerRef = ref<InstanceType<typeof createAndEditCaseDrawer>>();
function createCase() {
createAndEditCaseDrawerRef.value?.open();
createAndEditCaseDrawerRef.value?.open(props.apiDetail?.id as string);
}
function copyCase(record: ApiCaseDetail) {
createAndEditCaseDrawerRef.value?.open(record, true);
createAndEditCaseDrawerRef.value?.open(record.apiDefinitionId, record, true);
}
function openCaseTab(record: ApiCaseDetail) {
emit('openCaseTab', record);
}
const caseDetailDrawerVisible = ref(false);
const defaultCaseParams = inject<RequestParam>('defaultCaseParams');
const caseDetail = ref<Record<string, any>>({});
async function getCaseDetailInfo(id: string) {
try {
const res = await getCaseDetail(id);
const parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // id ;
// if (res.protocol === 'HTTP') { // TODO: protocol
// parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // id
// }
caseDetail.value = {
...cloneDeep(defaultCaseParams as RequestParam),
...({
...res.request,
...res,
// responseDefinition: res.response.map((e) => ({ ...e, responseActiveTab: ResponseComposition.BODY })), // TODO: response
url: res.path,
...parseRequestBodyResult,
} as Partial<TabItem>),
};
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
async function openCaseDetailDrawer(id: string) {
await getCaseDetailInfo(id);
caseDetailDrawerVisible.value = true;
}
// api
async function loadCase(id: string) {
getCaseDetailInfo(id);
loadCaseList();
}
</script>
<style lang="less" scoped>

View File

@ -1,14 +1,14 @@
<template>
<MsDrawer
v-model:visible="innerVisible"
:title="t('case.createCase')"
:title="isEdit ? t('case.updateCase') : t('case.createCase')"
:width="894"
no-content-padding
:ok-text="t('common.create')"
:ok-text="isEdit ? 'common.update' : 'common.create'"
:ok-loading="drawerLoading"
:save-continue-text="t('case.saveContinueText')"
:show-continue="true"
@confirm="handleDrawerConfirm"
:show-continue="!isEdit && !!props.apiDetail"
@confirm="handleDrawerConfirm(false)"
@continue="handleDrawerConfirm(true)"
@cancel="handleSaveCaseCancel"
>
@ -18,7 +18,7 @@
<div class="flex h-full flex-col overflow-hidden">
<div class="px-[16px] pt-[16px]">
<MsDetailCard
:title="`【${apiDataDetail.num}】${apiDataDetail.name}`"
:title="`【${apiDetailInfo.num}】${apiDetailInfo.name}`"
:description="description"
class="!flex-row justify-between"
>
@ -26,11 +26,11 @@
<apiMethodName :method="value as RequestMethods" tag-size="small" is-tag />
</template>
</MsDetailCard>
<a-form ref="formRef" class="mt-[16px]" :model="caseModalForm" layout="vertical">
<a-form ref="formRef" class="mt-[16px]" :model="detailForm" layout="vertical">
<a-form-item field="name" label="" :rules="[{ required: true, message: t('case.caseNameRequired') }]">
<div class="flex w-full items-center gap-[8px]">
<a-input
v-model:model-value="caseModalForm.name"
v-model:model-value="detailForm.name"
:placeholder="t('case.caseNamePlaceholder')"
allow-clear
:max-length="255"
@ -43,9 +43,9 @@
</a-form-item>
<div class="flex gap-[16px]">
<a-form-item field="priority" :label="t('case.caseLevel')">
<a-select v-model:model-value="caseModalForm.priority" :placeholder="t('common.pleaseSelect')">
<a-select v-model:model-value="detailForm.priority" :placeholder="t('common.pleaseSelect')">
<template #label>
<span class="text-[var(--color-text-2)]"> <caseLevel :case-level="caseModalForm.priority" /></span>
<span class="text-[var(--color-text-2)]"> <caseLevel :case-level="detailForm.priority" /></span>
</template>
<a-option v-for="item of casePriorityOptions" :key="item.value" :value="item.value">
<caseLevel :case-level="item.label as CaseLevel" />
@ -53,9 +53,9 @@
</a-select>
</a-form-item>
<a-form-item field="status" :label="t('apiTestManagement.apiStatus')">
<a-select v-model:model-value="caseModalForm.status" :placeholder="t('common.pleaseSelect')">
<a-select v-model:model-value="detailForm.status" :placeholder="t('common.pleaseSelect')">
<template #label>
<apiStatus :status="caseModalForm.status" />
<apiStatus :status="detailForm.status" />
</template>
<a-option v-for="item of Object.values(RequestDefinitionStatus)" :key="item" :value="item">
<apiStatus :status="item" />
@ -63,7 +63,7 @@
</a-select>
</a-form-item>
<a-form-item field="tags" :label="t('common.tag')">
<MsTagsInput v-model:model-value="caseModalForm.tags" />
<MsTagsInput v-model:model-value="detailForm.tags" />
</a-form-item>
</div>
</a-form>
@ -72,11 +72,11 @@
<div class="flex-1 overflow-hidden">
<requestComposition
ref="requestCompositionRef"
v-model:request="apiDataDetail"
v-model:request="detailForm"
:is-case="true"
hide-response-layout-switch
:upload-temp-file-api="uploadTempFileCase"
:file-save-as-source-id="apiDataDetail.id"
:file-save-as-source-id="detailForm.id"
:file-module-options-api="getTransferOptionsCase"
:file-save-as-api="transferFileCase"
:current-env-config="currentEnvConfig"
@ -103,41 +103,55 @@
import {
addCase,
getDefinitionDetail,
getTransferOptionsCase,
transferFileCase,
updateCase,
uploadTempFileCase,
} from '@/api/modules/api-test/management';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { ApiCaseDetail } from '@/models/apiTest/management';
import { AddApiCaseParams, ApiCaseDetail, ApiDefinitionDetail } from '@/models/apiTest/management';
import { EnvConfig } from '@/models/projectManagement/environmental';
import { RequestDefinitionStatus, RequestMethods } from '@/enums/apiEnum';
import { casePriorityOptions } from '@/views/api-test/components/config';
const props = defineProps<{
apiDetail: RequestParam;
apiDetail?: RequestParam | ApiDefinitionDetail;
}>();
const emit = defineEmits<{
(e: 'loadCase', id?: string): void;
}>();
const emit = defineEmits(['loadCase']);
const apiDataDetail = ref<RequestParam>(cloneDeep(props.apiDetail));
const { t } = useI18n();
const appStore = useAppStore();
const innerVisible = ref(false);
const drawerLoading = ref(false);
const apiDefinitionId = ref('');
const apiDetailInfo = ref<Record<string, any>>({});
async function getApiDetail() {
try {
apiDetailInfo.value = await getDefinitionDetail(apiDefinitionId.value);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
const description = computed(() => [
{
key: 'type',
locale: 'apiTestManagement.apiType',
value: apiDataDetail.value.method,
value: apiDetailInfo.value.method,
},
{
key: 'path',
locale: 'apiTestManagement.path',
value: apiDataDetail.value.url || apiDataDetail.value.path,
value: apiDetailInfo.value.url || apiDetailInfo.value.path,
},
]);
@ -145,47 +159,84 @@
const currentEnvConfig = computed<EnvConfig | undefined>(() => environmentSelectRef.value?.currentEnvConfig);
const formRef = ref<FormInstance>();
const initForm: any = {
apiDefinitionId: apiDataDetail.value.id as string,
name: '',
priority: 'P0',
tags: [],
status: RequestDefinitionStatus.PROCESSING,
};
const caseModalForm = ref({ ...initForm });
const requestCompositionRef = ref<InstanceType<typeof requestComposition>>();
const defaultCaseParams = inject<RequestParam>('defaultCaseParams');
const defaultDetail: RequestParam = {
apiDefinitionId: apiDefinitionId.value,
...(defaultCaseParams as RequestParam),
};
const detailForm = ref(cloneDeep(defaultDetail));
const isEdit = ref(false);
function open(record?: ApiCaseDetail, isCopy?: boolean) {
innerVisible.value = true;
if (isCopy) {
caseModalForm.value.name = record?.name;
function open(apiId: string, record?: ApiCaseDetail | RequestParam, isCopy?: boolean) {
apiDefinitionId.value = apiId;
// apiapicaseapi
if (props.apiDetail) {
apiDetailInfo.value = props.apiDetail;
} else {
getApiDetail();
}
//
if (isCopy) {
detailForm.value.name = `copy_${record?.name}`;
}
//
if (!isCopy && record?.id) {
isEdit.value = true;
detailForm.value = cloneDeep(record as RequestParam);
}
innerVisible.value = true;
}
function handleSaveCaseCancel() {
drawerLoading.value = false;
isEdit.value = false;
innerVisible.value = false;
formRef.value?.resetFields();
caseModalForm.value = { ...initForm };
detailForm.value = cloneDeep(defaultDetail);
}
function handleDrawerConfirm(isContinue: boolean) {
formRef.value?.validate(async (errors) => {
if (!errors) {
drawerLoading.value = true;
const params = { ...requestCompositionRef.value?.makeRequestParams(), ...caseModalForm.value };
//
if (!requestCompositionRef.value?.makeRequestParams()) return;
const { linkFileIds, uploadFileIds, request, unLinkFileIds, deleteFileIds } =
requestCompositionRef.value.makeRequestParams();
const { name, priority, status, tags, id } = detailForm.value;
const params: AddApiCaseParams = {
projectId: appStore.currentProjectId,
environmentId: currentEnvConfig.value?.id as string,
apiDefinitionId: apiDefinitionId.value,
linkFileIds,
uploadFileIds,
request,
id: id as string,
name,
priority,
status,
tags,
unLinkFileIds,
deleteFileIds,
};
try {
await addCase(params);
Message.success(t('common.updateSuccess'));
if (isEdit.value) {
await updateCase(params);
Message.success(t('common.updateSuccess'));
} else {
await addCase(params);
Message.success(t('common.createSuccess'));
}
emit('loadCase', id as string);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
if (!isContinue) {
emit('loadCase');
handleSaveCaseCancel();
}
caseModalForm.value = { ...initForm };
detailForm.value = cloneDeep(defaultDetail);
drawerLoading.value = false;
}
});

View File

@ -9,7 +9,13 @@
/>
</div>
<div v-if="activeApiTab.id !== 'all'" class="flex-1 overflow-hidden">
<caseDetail :active-api-tab="activeApiTab" :module-tree="props.moduleTree" />
<caseDetail
:detail="activeApiTab"
:module-tree="props.moduleTree"
:protocol="props.protocol"
@update-follow="activeApiTab.follow = !activeApiTab.follow"
@load-case="(id: string) => openOrUpdateCaseTab(false, id)"
/>
</div>
</div>
</template>
@ -24,9 +30,7 @@
import { ApiCaseDetail } from '@/models/apiTest/management';
import { ModuleTreeNode } from '@/models/common';
import { RequestAuthType, RequestComposition, RequestMethods, ResponseComposition } from '@/enums/apiEnum';
import { defaultBodyParams, defaultResponse, defaultResponseItem } from '@/views/api-test/components/config';
import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
import { parseRequestBodyFiles } from '@/views/api-test/components/utils';
@ -47,106 +51,37 @@
required: true,
});
const initDefaultId = `case-${Date.now()}`;
const defaultCaseParams: RequestParam = {
type: 'case',
id: initDefaultId,
moduleId: props.activeModule === 'all' ? 'root' : props.activeModule,
protocol: 'HTTP',
tags: [],
description: '',
url: '',
activeTab: RequestComposition.HEADER,
closable: true,
method: RequestMethods.GET,
headers: [],
body: cloneDeep(defaultBodyParams),
query: [],
rest: [],
polymorphicName: '',
name: '',
path: '',
projectId: '',
uploadFileIds: [],
linkFileIds: [],
authConfig: {
authType: RequestAuthType.NONE,
basicAuth: {
userName: '',
password: '',
},
digestAuth: {
userName: '',
password: '',
},
},
children: [
{
polymorphicName: 'MsCommonElement', // MsCommonElement
assertionConfig: {
enableGlobal: false,
assertions: [],
},
postProcessorConfig: {
enableGlobal: false,
processors: [],
},
preProcessorConfig: {
enableGlobal: false,
processors: [],
},
},
],
otherConfig: {
connectTimeout: 60000,
responseTimeout: 60000,
certificateAlias: '',
followRedirects: true,
autoRedirects: false,
},
responseActiveTab: ResponseComposition.BODY,
response: cloneDeep(defaultResponse),
responseDefinition: [cloneDeep(defaultResponseItem)],
isNew: true,
unSaved: false,
executeLoading: false,
preDependency: [], //
postDependency: [], //
};
function addTab(defaultProps?: Partial<TabItem>) {
apiTabs.value.push({
...cloneDeep(defaultCaseParams),
...defaultProps,
});
activeApiTab.value = apiTabs.value[apiTabs.value.length - 1];
}
const defaultCaseParams = inject<RequestParam>('defaultCaseParams');
const loading = ref(false);
async function openCaseTab(apiInfo: ApiCaseDetail) {
const isLoadedTabIndex = apiTabs.value.findIndex(
(e) => e.id === (typeof apiInfo === 'string' ? apiInfo : apiInfo.id)
);
if (isLoadedTabIndex > -1) {
// tabtab
activeApiTab.value = apiTabs.value[isLoadedTabIndex] as RequestParam;
return;
}
async function openOrUpdateCaseTab(isOpen: boolean, id: string) {
try {
loading.value = true;
const res = await getCaseDetail(typeof apiInfo === 'string' ? apiInfo : apiInfo.id);
const res = await getCaseDetail(id);
const parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // id ;
// if (res.protocol === 'HTTP') { // TODO: protocol
// parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // id
// }
addTab({
...res.request,
...res,
response: cloneDeep(defaultResponse),
// responseDefinition: res.response.map((e) => ({ ...e, responseActiveTab: ResponseComposition.BODY })), // TODO: response
url: res.path,
...parseRequestBodyResult,
});
const tabItemInfo = {
...cloneDeep(defaultCaseParams as RequestParam),
...({
...res.request,
...res,
// responseDefinition: res.response.map((e) => ({ ...e, responseActiveTab: ResponseComposition.BODY })), // TODO: response
url: res.path,
...parseRequestBodyResult,
} as Partial<TabItem>),
};
if (isOpen) {
apiTabs.value.push(tabItemInfo);
activeApiTab.value = apiTabs.value[apiTabs.value.length - 1];
} else {
//
const index = apiTabs.value.findIndex((item) => item.id === id);
apiTabs.value[index] = tabItemInfo;
activeApiTab.value = tabItemInfo;
}
nextTick(() => {
loading.value = false; // loading
});
@ -156,4 +91,16 @@
loading.value = false;
}
}
async function openCaseTab(apiInfo: ApiCaseDetail) {
const isLoadedTabIndex = apiTabs.value.findIndex(
(e) => e.id === (typeof apiInfo === 'string' ? apiInfo : apiInfo.id)
);
if (isLoadedTabIndex > -1) {
// tabtab
activeApiTab.value = apiTabs.value[isLoadedTabIndex] as RequestParam;
return;
}
await openOrUpdateCaseTab(true, typeof apiInfo === 'string' ? apiInfo : apiInfo.id);
}
</script>

View File

@ -58,6 +58,7 @@
<script setup lang="ts">
import { SelectOptionData } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import MsEditableTab from '@/components/pure/ms-editable-tab/index.vue';
import api from './api/index.vue';
@ -73,8 +74,17 @@
import { ModuleTreeNode } from '@/models/common';
import { EnvConfig } from '@/models/projectManagement/environmental';
import {
RequestAuthType,
RequestComposition,
RequestDefinitionStatus,
RequestMethods,
ResponseComposition,
} from '@/enums/apiEnum';
import { ProjectManagementRouteEnum } from '@/enums/routeEnum';
import { defaultBodyParams, defaultResponse, defaultResponseItem } from '@/views/api-test/components/config';
const props = defineProps<{
activeModule: string;
offspringIds: string[];
@ -113,6 +123,76 @@
]);
const activeApiTab = ref<RequestParam>(apiTabs.value[0] as RequestParam);
// apidefaultCaseParams
const initDefaultId = `case-${Date.now()}`;
const defaultCaseParams: RequestParam = {
id: initDefaultId,
type: 'case',
moduleId: props.activeModule === 'all' ? 'root' : props.activeModule,
protocol: 'HTTP',
tags: [],
description: '',
priority: 'P0',
status: RequestDefinitionStatus.PROCESSING,
url: '',
activeTab: RequestComposition.HEADER,
closable: true,
method: RequestMethods.GET,
headers: [],
body: cloneDeep(defaultBodyParams),
query: [],
rest: [],
polymorphicName: '',
name: '',
path: '',
projectId: '',
uploadFileIds: [],
linkFileIds: [],
authConfig: {
authType: RequestAuthType.NONE,
basicAuth: {
userName: '',
password: '',
},
digestAuth: {
userName: '',
password: '',
},
},
children: [
{
polymorphicName: 'MsCommonElement', // MsCommonElement
assertionConfig: {
enableGlobal: false,
assertions: [],
},
postProcessorConfig: {
enableGlobal: false,
processors: [],
},
preProcessorConfig: {
enableGlobal: false,
processors: [],
},
},
],
otherConfig: {
connectTimeout: 60000,
responseTimeout: 60000,
certificateAlias: '',
followRedirects: true,
autoRedirects: false,
},
responseActiveTab: ResponseComposition.BODY,
response: cloneDeep(defaultResponse),
responseDefinition: [cloneDeep(defaultResponseItem)],
isNew: true,
unSaved: false,
executeLoading: false,
preDependency: [], //
postDependency: [], //
};
// id
watch(
() => props.activeModule,
@ -236,6 +316,7 @@
/** 向孙组件提供属性 */
provide('currentEnvConfig', readonly(currentEnvConfig));
provide('defaultCaseParams', readonly(defaultCaseParams));
defineExpose({
newTab,

View File

@ -191,6 +191,7 @@ export default {
'case.recycle.recoverCaseTip': 'When restoring the case, the deleted API will be restored simultaneously.',
'case.recycle.confirmRecovery': 'Confirm recovery',
'case.createCase': 'Create Case',
'case.updateCase': 'Update Case',
'case.saveContinueText': 'Save & continue',
'case.detail.changeHistoryTip': `View and compare historical changes. According to the administrator's setting rules, historical changes will be automatically deleted`,
'case.detail.noReminders': 'No longer remind',

View File

@ -183,6 +183,7 @@ export default {
'case.recycle.recoverCaseTip': '恢复case时会同步恢复被删除的api',
'case.recycle.confirmRecovery': '确认恢复',
'case.createCase': '创建用例',
'case.updateCase': '更新用例',
'case.saveContinueText': '保存并继续创建',
'case.detail.changeHistoryTip': '查看、对比历史修改,根据管理员设置规则,变更历史数据将自动删除',
'case.detail.noReminders': '不再提醒',
@ -191,5 +192,4 @@ export default {
'case.detail.operator': '操作人',
'case.detail.tableColumnUpdateTime': '更新时间',
'case.detail.execute.success': '执行成功',
};