feat(接口管理): 接口定义优化
This commit is contained in:
parent
2b8cdc5359
commit
1d0ef9a28a
|
@ -14,7 +14,7 @@
|
||||||
:columns="jsonPathColumns"
|
:columns="jsonPathColumns"
|
||||||
:scroll="{ minWidth: '700px' }"
|
:scroll="{ minWidth: '700px' }"
|
||||||
:default-param-item="jsonPathDefaultParamItem"
|
:default-param-item="jsonPathDefaultParamItem"
|
||||||
@change="(data, isInit) => handleChange(data, !!isInit, 'jsonPath')"
|
@change="(data) => handleChange(data, 'jsonPath')"
|
||||||
@more-action-select="(e,r)=> handleExtractParamMoreActionSelect(e,r as ExpressionConfig)"
|
@more-action-select="(e,r)=> handleExtractParamMoreActionSelect(e,r as ExpressionConfig)"
|
||||||
>
|
>
|
||||||
<template #expression="{ record, rowIndex }">
|
<template #expression="{ record, rowIndex }">
|
||||||
|
@ -99,7 +99,7 @@
|
||||||
:columns="xPathColumns"
|
:columns="xPathColumns"
|
||||||
:scroll="{ minWidth: '700px' }"
|
:scroll="{ minWidth: '700px' }"
|
||||||
:default-param-item="xPathDefaultParamItem"
|
:default-param-item="xPathDefaultParamItem"
|
||||||
@change="(data, isInit) => handleChange(data, !!isInit, 'xPath')"
|
@change="(data) => handleChange(data, 'xPath')"
|
||||||
@more-action-select="(e,r)=> handleExtractParamMoreActionSelect(e,r as ExpressionConfig)"
|
@more-action-select="(e,r)=> handleExtractParamMoreActionSelect(e,r as ExpressionConfig)"
|
||||||
>
|
>
|
||||||
<template #expression="{ record, rowIndex }">
|
<template #expression="{ record, rowIndex }">
|
||||||
|
@ -193,7 +193,7 @@
|
||||||
:span-method="documentSpanMethod"
|
:span-method="documentSpanMethod"
|
||||||
is-tree-table
|
is-tree-table
|
||||||
@tree-delete="deleteAllParam"
|
@tree-delete="deleteAllParam"
|
||||||
@change="(data, isInit) => handleChange(data, !!isInit, 'document')"
|
@change="(data) => handleChange(data, 'document')"
|
||||||
>
|
>
|
||||||
<template #matchValueDelete="{ record }">
|
<template #matchValueDelete="{ record }">
|
||||||
<icon-minus-circle
|
<icon-minus-circle
|
||||||
|
@ -234,7 +234,7 @@
|
||||||
:columns="xPathColumns"
|
:columns="xPathColumns"
|
||||||
:scroll="{ minWidth: '700px' }"
|
:scroll="{ minWidth: '700px' }"
|
||||||
:default-param-item="xPathDefaultParamItem"
|
:default-param-item="xPathDefaultParamItem"
|
||||||
@change="(data, isInit) => handleChange(data, !!isInit, 'regular')"
|
@change="(data) => handleChange(data, 'regular')"
|
||||||
@more-action-select="(e,r)=> handleExtractParamMoreActionSelect(e,r as ExpressionConfig)"
|
@more-action-select="(e,r)=> handleExtractParamMoreActionSelect(e,r as ExpressionConfig)"
|
||||||
>
|
>
|
||||||
<template #expression="{ record, rowIndex }">
|
<template #expression="{ record, rowIndex }">
|
||||||
|
@ -473,10 +473,7 @@
|
||||||
matchValue: '',
|
matchValue: '',
|
||||||
enable: true,
|
enable: true,
|
||||||
};
|
};
|
||||||
const handleChange = (data: any[], isInit: boolean, type: string) => {
|
const handleChange = (data: any[], type: string) => {
|
||||||
if (isInit) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (type === 'jsonPath') {
|
if (type === 'jsonPath') {
|
||||||
innerParams.value.jsonPath = data;
|
innerParams.value.jsonPath = data;
|
||||||
} else if (type === 'xPath') {
|
} else if (type === 'xPath') {
|
||||||
|
|
|
@ -20,7 +20,13 @@
|
||||||
@click="handleTabClick(tab)"
|
@click="handleTabClick(tab)"
|
||||||
>
|
>
|
||||||
<div :draggable="!!tab.draggable" class="flex items-center">
|
<div :draggable="!!tab.draggable" class="flex items-center">
|
||||||
<slot name="label" :tab="tab">{{ tab.label }}</slot>
|
<slot name="label" :tab="tab">
|
||||||
|
<a-tooltip :content="tab.label" :mouse-enter-delay="500">
|
||||||
|
<div class="one-line-text flex max-w-[144px] items-center">
|
||||||
|
{{ tab.label }}
|
||||||
|
</div>
|
||||||
|
</a-tooltip>
|
||||||
|
</slot>
|
||||||
<div v-if="tab.unSaved" class="ml-[8px] h-[8px] w-[8px] rounded-full bg-[rgb(var(--primary-5))]"></div>
|
<div v-if="tab.unSaved" class="ml-[8px] h-[8px] w-[8px] rounded-full bg-[rgb(var(--primary-5))]"></div>
|
||||||
<MsButton
|
<MsButton
|
||||||
v-if="props.atLeastOne ? props.tabs.length > 1 && tab.closable : tab.closable !== false"
|
v-if="props.atLeastOne ? props.tabs.length > 1 && tab.closable : tab.closable !== false"
|
||||||
|
|
|
@ -551,7 +551,7 @@
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'change', data: any[], isInit?: boolean): void; // 都触发这个事件以通知父组件参数数组被更改
|
(e: 'change', data: any[]): void; // 都触发这个事件以通知父组件参数数组被更改
|
||||||
(e: 'moreActionSelect', event: ActionsItem, record: Record<string, any>): void;
|
(e: 'moreActionSelect', event: ActionsItem, record: Record<string, any>): void;
|
||||||
(e: 'projectChange', projectId: string): void;
|
(e: 'projectChange', projectId: string): void;
|
||||||
(e: 'treeDelete', record: Record<string, any>): void;
|
(e: 'treeDelete', record: Record<string, any>): void;
|
||||||
|
@ -583,6 +583,12 @@
|
||||||
showPagination: false,
|
showPagination: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function emitChange(from: string, isInit?: boolean) {
|
||||||
|
if (!isInit) {
|
||||||
|
emit('change', propsRes.value.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const selectedKeys = computed(() => propsRes.value.data.filter((e) => e.enable).map((e) => e.id));
|
const selectedKeys = computed(() => propsRes.value.data.filter((e) => e.enable).map((e) => e.id));
|
||||||
propsEvent.value.rowSelectChange = (key: string) => {
|
propsEvent.value.rowSelectChange = (key: string) => {
|
||||||
propsRes.value.data = propsRes.value.data.map((e) => {
|
propsRes.value.data = propsRes.value.data.map((e) => {
|
||||||
|
@ -591,14 +597,14 @@
|
||||||
}
|
}
|
||||||
return e;
|
return e;
|
||||||
});
|
});
|
||||||
emit('change', propsRes.value.data);
|
emitChange('rowSelectChange');
|
||||||
};
|
};
|
||||||
propsEvent.value.selectAllChange = (v: SelectAllEnum) => {
|
propsEvent.value.selectAllChange = (v: SelectAllEnum) => {
|
||||||
propsRes.value.data = propsRes.value.data.map((e) => {
|
propsRes.value.data = propsRes.value.data.map((e) => {
|
||||||
e.enable = v !== SelectAllEnum.NONE;
|
e.enable = v !== SelectAllEnum.NONE;
|
||||||
return e;
|
return e;
|
||||||
});
|
});
|
||||||
emit('change', propsRes.value.data);
|
emitChange('selectAllChange');
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
@ -623,7 +629,7 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
propsRes.value.data.splice(rowIndex, 1);
|
propsRes.value.data.splice(rowIndex, 1);
|
||||||
emit('change', propsRes.value.data);
|
emitChange('deleteParam');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 断言-文档-Begin */
|
/** 断言-文档-Begin */
|
||||||
|
@ -638,7 +644,7 @@
|
||||||
e.mustInclude = val;
|
e.mustInclude = val;
|
||||||
});
|
});
|
||||||
propsRes.value.data = data;
|
propsRes.value.data = data;
|
||||||
emit('change', propsRes.value.data);
|
emitChange('handleMustIncludeChange');
|
||||||
};
|
};
|
||||||
const handleMustContainColChange = (notEmit?: boolean) => {
|
const handleMustContainColChange = (notEmit?: boolean) => {
|
||||||
const { data } = propsRes.value;
|
const { data } = propsRes.value;
|
||||||
|
@ -654,7 +660,7 @@
|
||||||
mustIncludeIndeterminate.value = true;
|
mustIncludeIndeterminate.value = true;
|
||||||
}
|
}
|
||||||
if (!notEmit) {
|
if (!notEmit) {
|
||||||
emit('change', propsRes.value.data);
|
emitChange('handleMustContainColChange');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -668,7 +674,7 @@
|
||||||
e.typeChecking = val;
|
e.typeChecking = val;
|
||||||
});
|
});
|
||||||
propsRes.value.data = data;
|
propsRes.value.data = data;
|
||||||
emit('change', propsRes.value.data);
|
emitChange('handleTypeCheckingChange');
|
||||||
};
|
};
|
||||||
const handleTypeCheckingColChange = (notEmit?: boolean) => {
|
const handleTypeCheckingColChange = (notEmit?: boolean) => {
|
||||||
const { data } = propsRes.value;
|
const { data } = propsRes.value;
|
||||||
|
@ -684,7 +690,7 @@
|
||||||
typeCheckingIndeterminate.value = true;
|
typeCheckingIndeterminate.value = true;
|
||||||
}
|
}
|
||||||
if (!notEmit) {
|
if (!notEmit) {
|
||||||
emit('change', propsRes.value.data);
|
emitChange('handleTypeCheckingColChange');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
/** 断言-文档-end */
|
/** 断言-文档-end */
|
||||||
|
@ -707,7 +713,7 @@
|
||||||
|
|
||||||
const handleEnvironment = (obj: Record<string, any>, record: Record<string, any>) => {
|
const handleEnvironment = (obj: Record<string, any>, record: Record<string, any>) => {
|
||||||
record.domain = {};
|
record.domain = {};
|
||||||
emit('change', propsRes.value.data);
|
emitChange('handleEnvironment');
|
||||||
};
|
};
|
||||||
|
|
||||||
const hostVisible = ref(false);
|
const hostVisible = ref(false);
|
||||||
|
@ -751,7 +757,7 @@
|
||||||
* @param key 当前列的 key
|
* @param key 当前列的 key
|
||||||
* @param isForce 是否强制添加
|
* @param isForce 是否强制添加
|
||||||
*/
|
*/
|
||||||
function addTableLine(rowIndex: number, addLineDisabled?: boolean) {
|
function addTableLine(rowIndex: number, addLineDisabled?: boolean, isInit?: boolean) {
|
||||||
if (addLineDisabled) {
|
if (addLineDisabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -763,7 +769,7 @@
|
||||||
...cloneDeep(props.defaultParamItem), // 深拷贝,避免有嵌套引用类型,数据隔离
|
...cloneDeep(props.defaultParamItem), // 深拷贝,避免有嵌套引用类型,数据隔离
|
||||||
enable: true, // 是否勾选
|
enable: true, // 是否勾选
|
||||||
} as any);
|
} as any);
|
||||||
emit('change', propsRes.value.data);
|
emitChange('addTableLine', isInit);
|
||||||
}
|
}
|
||||||
handleMustContainColChange(true);
|
handleMustContainColChange(true);
|
||||||
handleTypeCheckingColChange(true);
|
handleTypeCheckingColChange(true);
|
||||||
|
@ -786,7 +792,7 @@
|
||||||
return item;
|
return item;
|
||||||
});
|
});
|
||||||
if (hasNoIdItem && !filterKeyValParams(arr, props.defaultParamItem).lastDataIsDefault && !props.isTreeTable) {
|
if (hasNoIdItem && !filterKeyValParams(arr, props.defaultParamItem).lastDataIsDefault && !props.isTreeTable) {
|
||||||
addTableLine(arr.length - 1);
|
addTableLine(arr.length - 1, false, true);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const id = new Date().getTime().toString();
|
const id = new Date().getTime().toString();
|
||||||
|
@ -797,7 +803,7 @@
|
||||||
enable: true, // 是否勾选
|
enable: true, // 是否勾选
|
||||||
},
|
},
|
||||||
] as any[];
|
] as any[];
|
||||||
emit('change', propsRes.value.data, true);
|
emitChange('watch props.params', true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -808,7 +814,7 @@
|
||||||
function toggleRequired(record: Record<string, any>, rowIndex: number) {
|
function toggleRequired(record: Record<string, any>, rowIndex: number) {
|
||||||
record.required = !record.required;
|
record.required = !record.required;
|
||||||
addTableLine(rowIndex);
|
addTableLine(rowIndex);
|
||||||
emit('change', propsRes.value.data);
|
emitChange('toggleRequired');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleFileChange(
|
async function handleFileChange(
|
||||||
|
@ -842,7 +848,7 @@
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
addTableLine(rowIndex);
|
addTableLine(rowIndex);
|
||||||
emit('change', propsRes.value.data);
|
emitChange('handleFileChange');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log(error);
|
console.log(error);
|
||||||
|
@ -871,7 +877,7 @@
|
||||||
showQuickInputParam.value = false;
|
showQuickInputParam.value = false;
|
||||||
addTableLine(propsRes.value.data.findIndex((e) => e.id === activeQuickInputRecord.value.id));
|
addTableLine(propsRes.value.data.findIndex((e) => e.id === activeQuickInputRecord.value.id));
|
||||||
clearQuickInputParam();
|
clearQuickInputParam();
|
||||||
emit('change', propsRes.value.data);
|
emitChange('applyQuickInputParam');
|
||||||
}
|
}
|
||||||
|
|
||||||
const showQuickInputDesc = ref(false);
|
const showQuickInputDesc = ref(false);
|
||||||
|
@ -893,11 +899,11 @@
|
||||||
showQuickInputDesc.value = false;
|
showQuickInputDesc.value = false;
|
||||||
addTableLine(propsRes.value.data.findIndex((e) => e.id === activeQuickInputRecord.value.id));
|
addTableLine(propsRes.value.data.findIndex((e) => e.id === activeQuickInputRecord.value.id));
|
||||||
clearQuickInputDesc();
|
clearQuickInputDesc();
|
||||||
emit('change', propsRes.value.data);
|
emitChange('applyQuickInputDesc');
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDescChange() {
|
function handleDescChange() {
|
||||||
emit('change', propsRes.value.data);
|
emitChange('handleDescChange');
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTypeChange(
|
function handleTypeChange(
|
||||||
|
@ -917,7 +923,7 @@
|
||||||
record.contentType = RequestContentTypeEnum.TEXT;
|
record.contentType = RequestContentTypeEnum.TEXT;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
emit('change', propsRes.value.data);
|
emitChange('handleTypeChange');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
</a-empty>
|
</a-empty>
|
||||||
<div v-show="!pluginError || isHttpProtocol" class="flex h-full flex-col">
|
<div v-show="!pluginError || isHttpProtocol" class="flex h-full flex-col">
|
||||||
<div class="px-[18px] pt-[8px]">
|
<div class="px-[18px] pt-[8px]">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex flex-wrap items-center justify-between gap-[12px]">
|
||||||
<div class="flex flex-1 items-center gap-[16px]">
|
<div class="flex flex-1 items-center gap-[16px]">
|
||||||
<a-select
|
<a-select
|
||||||
v-if="requestVModel.isNew"
|
v-if="requestVModel.isNew"
|
||||||
|
@ -54,41 +54,77 @@
|
||||||
/>
|
/>
|
||||||
</a-input-group>
|
</a-input-group>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-[16px]">
|
<div>
|
||||||
<a-dropdown-button
|
<a-radio-group
|
||||||
v-if="!requestVModel.executeLoading && hasAnyPermission([props.permissionMap.execute])"
|
v-if="props.isDefinition"
|
||||||
:disabled="requestVModel.executeLoading || (isHttpProtocol && !requestVModel.url)"
|
v-model:model-value="requestVModel.mode"
|
||||||
class="exec-btn"
|
type="button"
|
||||||
@click="() => execute(isPriorityLocalExec ? 'localExec' : 'serverExec')"
|
class="mr-[12px]"
|
||||||
@select="execute"
|
|
||||||
>
|
>
|
||||||
{{ isPriorityLocalExec ? t('apiTestDebug.localExec') : t('apiTestDebug.serverExec') }}
|
<a-radio value="definition">{{ t('apiTestManagement.definition') }}</a-radio>
|
||||||
<template v-if="hasLocalExec" #icon>
|
<a-radio value="debug">{{ t('apiTestManagement.debug') }}</a-radio>
|
||||||
<icon-down />
|
</a-radio-group>
|
||||||
</template>
|
<!-- 接口定义-调试模式下,可执行 -->
|
||||||
<template v-if="hasLocalExec" #content>
|
<template
|
||||||
<a-doption :value="isPriorityLocalExec ? 'serverExec' : 'localExec'">
|
v-if="
|
||||||
{{ isPriorityLocalExec ? t('apiTestDebug.serverExec') : t('apiTestDebug.localExec') }}
|
(!props.isDefinition || (props.isDefinition && requestVModel.mode === 'debug')) &&
|
||||||
</a-doption>
|
hasAnyPermission([props.permissionMap.execute])
|
||||||
</template>
|
"
|
||||||
</a-dropdown-button>
|
|
||||||
<a-button v-else type="primary" class="mr-[12px]" @click="stopDebug">{{ t('common.stop') }}</a-button>
|
|
||||||
<a-dropdown
|
|
||||||
v-if="props.isDefinition && hasAnyPermission([props.permissionMap.create, props.permissionMap.update])"
|
|
||||||
:loading="saveLoading || (isHttpProtocol && !requestVModel.url)"
|
|
||||||
@select="handleSelect"
|
|
||||||
>
|
>
|
||||||
<a-button type="secondary">
|
<a-dropdown-button
|
||||||
|
v-if="!requestVModel.executeLoading"
|
||||||
|
:disabled="requestVModel.executeLoading || (isHttpProtocol && !requestVModel.url)"
|
||||||
|
class="exec-btn"
|
||||||
|
@click="() => execute(isPriorityLocalExec ? 'localExec' : 'serverExec')"
|
||||||
|
@select="execute"
|
||||||
|
>
|
||||||
|
{{ isPriorityLocalExec ? t('apiTestDebug.localExec') : t('apiTestDebug.serverExec') }}
|
||||||
|
<template v-if="hasLocalExec" #icon>
|
||||||
|
<icon-down />
|
||||||
|
</template>
|
||||||
|
<template v-if="hasLocalExec" #content>
|
||||||
|
<a-doption :value="isPriorityLocalExec ? 'serverExec' : 'localExec'">
|
||||||
|
{{ isPriorityLocalExec ? t('apiTestDebug.serverExec') : t('apiTestDebug.localExec') }}
|
||||||
|
</a-doption>
|
||||||
|
</template>
|
||||||
|
</a-dropdown-button>
|
||||||
|
<a-button v-else type="primary" class="mr-[12px]" @click="stopDebug">{{ t('common.stop') }}</a-button>
|
||||||
|
</template>
|
||||||
|
<!-- 接口定义-且有保存或更新权限 -->
|
||||||
|
<template
|
||||||
|
v-if="
|
||||||
|
props.isDefinition &&
|
||||||
|
(requestVModel.isNew
|
||||||
|
? hasAnyPermission([props.permissionMap.create])
|
||||||
|
: hasAnyPermission([props.permissionMap.update]))
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<!-- 接口定义-调试模式,可保存或保存为新用例 -->
|
||||||
|
<a-dropdown
|
||||||
|
v-if="requestVModel.mode === 'debug'"
|
||||||
|
:loading="saveLoading || (isHttpProtocol && !requestVModel.url)"
|
||||||
|
@select="handleSelect"
|
||||||
|
>
|
||||||
|
<a-button type="secondary">
|
||||||
|
{{ t('common.save') }}
|
||||||
|
</a-button>
|
||||||
|
<template #content>
|
||||||
|
<a-doption value="save">{{ t('common.save') }}</a-doption>
|
||||||
|
<a-doption value="saveAsCase">{{ t('apiTestManagement.saveAsCase') }}</a-doption>
|
||||||
|
</template>
|
||||||
|
</a-dropdown>
|
||||||
|
<!-- 接口定义-定义模式,直接保存接口定义 -->
|
||||||
|
<a-button v-else type="primary" @click="() => handleSelect('save')">
|
||||||
{{ t('common.save') }}
|
{{ t('common.save') }}
|
||||||
</a-button>
|
</a-button>
|
||||||
<template #content>
|
</template>
|
||||||
<a-doption value="save">{{ t('common.save') }}</a-doption>
|
<!-- 接口调试,支持快捷保存 -->
|
||||||
<a-doption value="saveAsCase">{{ t('apiTestManagement.saveAsCase') }}</a-doption>
|
|
||||||
</template>
|
|
||||||
</a-dropdown>
|
|
||||||
<a-button
|
<a-button
|
||||||
v-else
|
v-else-if="
|
||||||
v-permission="[props.permissionMap.create, props.permissionMap.update]"
|
requestVModel.isNew
|
||||||
|
? hasAnyPermission([props.permissionMap.create])
|
||||||
|
: hasAnyPermission([props.permissionMap.update])
|
||||||
|
"
|
||||||
type="secondary"
|
type="secondary"
|
||||||
:disabled="isHttpProtocol && !requestVModel.url"
|
:disabled="isHttpProtocol && !requestVModel.url"
|
||||||
:loading="saveLoading"
|
:loading="saveLoading"
|
||||||
|
@ -102,7 +138,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div ref="splitContainerRef" :class="splitContainerClass">
|
<div ref="splitContainerRef" class="h-[calc(100%-40px)]">
|
||||||
<MsSplitBox
|
<MsSplitBox
|
||||||
ref="splitBoxRef"
|
ref="splitBoxRef"
|
||||||
v-model:size="splitBoxSize"
|
v-model:size="splitBoxSize"
|
||||||
|
@ -138,7 +174,13 @@
|
||||||
v-model:api="fApi"
|
v-model:api="fApi"
|
||||||
:rule="currentPluginScript"
|
:rule="currentPluginScript"
|
||||||
:option="currentPluginOptions"
|
:option="currentPluginOptions"
|
||||||
@change="handlePluginFormChange"
|
@change="
|
||||||
|
() => {
|
||||||
|
if (isInitPluginForm) {
|
||||||
|
handlePluginFormChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</a-spin>
|
</a-spin>
|
||||||
<httpHeader
|
<httpHeader
|
||||||
|
@ -329,6 +371,8 @@
|
||||||
isNew: boolean;
|
isNew: boolean;
|
||||||
protocol: string;
|
protocol: string;
|
||||||
activeTab: RequestComposition;
|
activeTab: RequestComposition;
|
||||||
|
mode?: 'definition' | 'debug';
|
||||||
|
executeLoading: boolean; // 执行中loading
|
||||||
}
|
}
|
||||||
export type RequestParam = ExecuteApiRequestFullParams & {
|
export type RequestParam = ExecuteApiRequestFullParams & {
|
||||||
responseDefinition?: ResponseItem[];
|
responseDefinition?: ResponseItem[];
|
||||||
|
@ -364,7 +408,7 @@
|
||||||
|
|
||||||
const loading = defineModel<boolean>('detailLoading', { default: false });
|
const loading = defineModel<boolean>('detailLoading', { default: false });
|
||||||
const requestVModel = defineModel<RequestParam>('request', { required: true });
|
const requestVModel = defineModel<RequestParam>('request', { required: true });
|
||||||
requestVModel.value.executeLoading = false; // 注册loading
|
|
||||||
const isHttpProtocol = computed(() => requestVModel.value.protocol === 'HTTP');
|
const isHttpProtocol = computed(() => requestVModel.value.protocol === 'HTTP');
|
||||||
const temporaryResponseMap = {}; // 缓存websocket返回的报告内容,避免执行接口后切换tab导致报告丢失
|
const temporaryResponseMap = {}; // 缓存websocket返回的报告内容,避免执行接口后切换tab导致报告丢失
|
||||||
const isInitPluginForm = ref(false);
|
const isInitPluginForm = ref(false);
|
||||||
|
@ -392,7 +436,7 @@
|
||||||
const commonContentTabKey = [
|
const commonContentTabKey = [
|
||||||
RequestComposition.PRECONDITION,
|
RequestComposition.PRECONDITION,
|
||||||
RequestComposition.POST_CONDITION,
|
RequestComposition.POST_CONDITION,
|
||||||
// RequestComposition.ASSERTION, TODO:断言暂时无
|
RequestComposition.ASSERTION,
|
||||||
];
|
];
|
||||||
// 请求内容插件tab
|
// 请求内容插件tab
|
||||||
const pluginContentTab = [
|
const pluginContentTab = [
|
||||||
|
@ -443,10 +487,20 @@
|
||||||
// 根据协议类型获取请求内容tab
|
// 根据协议类型获取请求内容tab
|
||||||
const contentTabList = computed(() => {
|
const contentTabList = computed(() => {
|
||||||
if (isHttpProtocol.value) {
|
if (isHttpProtocol.value) {
|
||||||
|
if (props.isDefinition) {
|
||||||
|
// 接口定义,定义模式隐藏前后置、断言
|
||||||
|
return requestVModel.value.mode === 'debug'
|
||||||
|
? httpContentTabList
|
||||||
|
: httpContentTabList.filter((e) => !commonContentTabKey.includes(e.value));
|
||||||
|
}
|
||||||
// 接口调试无断言
|
// 接口调试无断言
|
||||||
return props.isDefinition
|
return httpContentTabList.filter((e) => e.value !== RequestComposition.ASSERTION);
|
||||||
? httpContentTabList
|
}
|
||||||
: httpContentTabList.filter((e) => e.value !== RequestComposition.ASSERTION);
|
if (props.isDefinition) {
|
||||||
|
// 接口定义,定义模式隐藏前后置、断言
|
||||||
|
return requestVModel.value.mode === 'definition'
|
||||||
|
? pluginContentTab
|
||||||
|
: [...pluginContentTab, ...httpContentTabList.filter((e) => commonContentTabKey.includes(e.value))];
|
||||||
}
|
}
|
||||||
return [...pluginContentTab, ...httpContentTabList.filter((e) => commonContentTabKey.includes(e.value))];
|
return [...pluginContentTab, ...httpContentTabList.filter((e) => commonContentTabKey.includes(e.value))];
|
||||||
});
|
});
|
||||||
|
@ -536,6 +590,24 @@
|
||||||
handleActiveDebugChange();
|
handleActiveDebugChange();
|
||||||
}, 300);
|
}, 300);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 控制插件表单字段显示
|
||||||
|
*/
|
||||||
|
function controlPluginFormFields() {
|
||||||
|
const allFields = fApi.value?.fields();
|
||||||
|
let fields: string[] = [];
|
||||||
|
if (props.isDefinition) {
|
||||||
|
// 接口定义使用接口定义的字段集
|
||||||
|
// 根据 apiDefinitionFields 字段集合展示需要的字段,隐藏其他字段
|
||||||
|
fields = pluginScriptMap.value[requestVModel.value.protocol].apiDefinitionFields || [];
|
||||||
|
} else {
|
||||||
|
// 根据 apiDebugFields 字段集合展示需要的字段,隐藏其他字段
|
||||||
|
fields = pluginScriptMap.value[requestVModel.value.protocol].apiDebugFields || [];
|
||||||
|
}
|
||||||
|
fApi.value?.hidden(true, allFields?.filter((e) => !fields.includes(e)) || []);
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置插件表单数据
|
* 设置插件表单数据
|
||||||
*/
|
*/
|
||||||
|
@ -543,30 +615,26 @@
|
||||||
const tempForm = temporaryPluginFormMap[requestVModel.value.id];
|
const tempForm = temporaryPluginFormMap[requestVModel.value.id];
|
||||||
if (tempForm || !requestVModel.value.isNew) {
|
if (tempForm || !requestVModel.value.isNew) {
|
||||||
// 如果缓存的表单数据存在或者是编辑状态,则需要将之前的输入数据填充
|
// 如果缓存的表单数据存在或者是编辑状态,则需要将之前的输入数据填充
|
||||||
fApi.value?.nextRefresh(() => {
|
const formData = tempForm || requestVModel.value;
|
||||||
fApi.value?.reload(currentPluginScript.value);
|
if (fApi.value) {
|
||||||
const formData = tempForm || requestVModel.value;
|
fApi.value.nextTick(() => {
|
||||||
if (fApi.value) {
|
|
||||||
const form = {};
|
const form = {};
|
||||||
if (props.isDefinition) {
|
controlPluginFormFields().forEach((key) => {
|
||||||
// 接口定义使用接口定义的字段集
|
form[key] = formData[key];
|
||||||
pluginScriptMap.value[requestVModel.value.protocol].apiDefinitionFields?.forEach((key) => {
|
});
|
||||||
form[key] = formData[key];
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
pluginScriptMap.value[requestVModel.value.protocol].apiDebugFields?.forEach((key) => {
|
|
||||||
form[key] = formData[key];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
fApi.value?.setValue(form);
|
fApi.value?.setValue(form);
|
||||||
}
|
setTimeout(() => {
|
||||||
});
|
// 初始化时赋值会触发表单数据变更,300ms 是为了与 handlePluginFormChange的防抖时间保持一致
|
||||||
nextTick(() => {
|
isInitPluginForm.value = true;
|
||||||
isInitPluginForm.value = true;
|
}, 300);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// 如果是没有缓存也不是编辑,则需要重置表单,因为 form-create 只有一个实例,已经被其他有数据的 tab 污染了,需要重置
|
fApi.value?.nextTick(() => {
|
||||||
|
controlPluginFormFields();
|
||||||
|
});
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
|
// 如果是没有缓存也不是编辑,则需要重置表单,因为 form-create 只有一个实例,已经被其他有数据的 tab 污染了,需要重置
|
||||||
fApi.value?.resetFields();
|
fApi.value?.resetFields();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -582,6 +650,7 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
pluginError.value = false;
|
pluginError.value = false;
|
||||||
|
isInitPluginForm.value = false;
|
||||||
if (pluginScriptMap.value[requestVModel.value.protocol] !== undefined) {
|
if (pluginScriptMap.value[requestVModel.value.protocol] !== undefined) {
|
||||||
setPluginFormData();
|
setPluginFormData();
|
||||||
// 已经初始化过
|
// 已经初始化过
|
||||||
|
@ -672,12 +741,6 @@
|
||||||
const activeLayout = ref<'horizontal' | 'vertical'>('vertical');
|
const activeLayout = ref<'horizontal' | 'vertical'>('vertical');
|
||||||
const splitContainerRef = ref<HTMLElement>();
|
const splitContainerRef = ref<HTMLElement>();
|
||||||
const secondBoxHeight = ref(0);
|
const secondBoxHeight = ref(0);
|
||||||
const splitContainerClass = computed(() => {
|
|
||||||
if (!showResponse.value) {
|
|
||||||
return 'h-full';
|
|
||||||
}
|
|
||||||
return 'h-[calc(100%-40px)]';
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => splitBoxSize.value,
|
() => splitBoxSize.value,
|
||||||
|
@ -716,6 +779,17 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => showResponse.value,
|
||||||
|
(val) => {
|
||||||
|
if (val) {
|
||||||
|
changeExpand(true);
|
||||||
|
} else {
|
||||||
|
changeExpand(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
function handleActiveLayoutChange() {
|
function handleActiveLayoutChange() {
|
||||||
isExpanded.value = true;
|
isExpanded.value = true;
|
||||||
splitBoxSize.value = 0.6;
|
splitBoxSize.value = 0.6;
|
||||||
|
@ -950,6 +1024,7 @@
|
||||||
const res = await props.createApi({
|
const res = await props.createApi({
|
||||||
...makeRequestParams(),
|
...makeRequestParams(),
|
||||||
...saveModalForm.value,
|
...saveModalForm.value,
|
||||||
|
path: isHttpProtocol.value ? saveModalForm.value.path : undefined,
|
||||||
...props.otherParams,
|
...props.otherParams,
|
||||||
});
|
});
|
||||||
requestVModel.value.id = res.id;
|
requestVModel.value.id = res.id;
|
||||||
|
@ -979,33 +1054,29 @@
|
||||||
*/
|
*/
|
||||||
async function handleSaveShortcut() {
|
async function handleSaveShortcut() {
|
||||||
if (!requestVModel.value.isNew) {
|
if (!requestVModel.value.isNew) {
|
||||||
if (hasAnyPermission([props.permissionMap.update])) {
|
// 更新接口不需要弹窗,直接更新保存
|
||||||
// 更新接口不需要弹窗,直接更新保存
|
updateDebug();
|
||||||
updateDebug();
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (hasAnyPermission([props.permissionMap.create])) {
|
try {
|
||||||
try {
|
if (!isHttpProtocol.value) {
|
||||||
if (!isHttpProtocol.value) {
|
// 插件需要校验动态表单
|
||||||
// 插件需要校验动态表单
|
await fApi.value?.validate();
|
||||||
await fApi.value?.validate();
|
|
||||||
}
|
|
||||||
saveModalForm.value = {
|
|
||||||
name: requestVModel.value.name || '',
|
|
||||||
path: requestVModel.value.url || '',
|
|
||||||
moduleId: 'root',
|
|
||||||
};
|
|
||||||
saveModalVisible.value = true;
|
|
||||||
} catch (error) {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log(error);
|
|
||||||
// 校验不通过则不进行保存
|
|
||||||
requestVModel.value.activeTab = RequestComposition.PLUGIN;
|
|
||||||
nextTick(() => {
|
|
||||||
scrollIntoView(document.querySelector('.arco-form-item-message'), { block: 'center' });
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
saveModalForm.value = {
|
||||||
|
name: requestVModel.value.name || '',
|
||||||
|
path: requestVModel.value.url || '',
|
||||||
|
moduleId: 'root',
|
||||||
|
};
|
||||||
|
saveModalVisible.value = true;
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(error);
|
||||||
|
// 校验不通过则不进行保存
|
||||||
|
requestVModel.value.activeTab = RequestComposition.PLUGIN;
|
||||||
|
nextTick(() => {
|
||||||
|
scrollIntoView(document.querySelector('.arco-form-item-message'), { block: 'center' });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -60,7 +60,7 @@
|
||||||
class="flex items-center justify-between gap-[24px]"
|
class="flex items-center justify-between gap-[24px]"
|
||||||
>
|
>
|
||||||
<a-popover position="left" content-class="response-popover-content">
|
<a-popover position="left" content-class="response-popover-content">
|
||||||
<div :style="{ color: statusCodeColor }">
|
<div class="one-line-text max-w-[200px]" :style="{ color: statusCodeColor }">
|
||||||
{{ props.requestTaskResult.requestResults[0].responseResult.responseCode }}
|
{{ props.requestTaskResult.requestResults[0].responseResult.responseCode }}
|
||||||
</div>
|
</div>
|
||||||
<template #content>
|
<template #content>
|
||||||
|
@ -225,6 +225,15 @@
|
||||||
function setActiveResponse(val: 'content' | 'result') {
|
function setActiveResponse(val: 'content' | 'result') {
|
||||||
activeResponseType.value = val;
|
activeResponseType.value = val;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.requestTaskResult,
|
||||||
|
(task) => {
|
||||||
|
if (task) {
|
||||||
|
setActiveResponse('result');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
|
|
|
@ -249,6 +249,7 @@
|
||||||
responseActiveTab: ResponseComposition.BODY,
|
responseActiveTab: ResponseComposition.BODY,
|
||||||
response: cloneDeep(defaultResponse),
|
response: cloneDeep(defaultResponse),
|
||||||
isNew: true,
|
isNew: true,
|
||||||
|
executeLoading: false,
|
||||||
};
|
};
|
||||||
const debugTabs = ref<RequestParam[]>([cloneDeep(defaultDebugParams)]);
|
const debugTabs = ref<RequestParam[]>([cloneDeep(defaultDebugParams)]);
|
||||||
const activeDebug = ref<RequestParam>(debugTabs.value[0]);
|
const activeDebug = ref<RequestParam>(debugTabs.value[0]);
|
||||||
|
|
|
@ -6,21 +6,6 @@
|
||||||
{{ t('apiTestManagement.showSubdirectory') }}
|
{{ t('apiTestManagement.showSubdirectory') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-[8px]">
|
<div class="flex items-center gap-[8px]">
|
||||||
<template v-if="!props.readOnly">
|
|
||||||
<a-button type="outline" class="arco-btn-outline--secondary !p-[8px]">
|
|
||||||
<template #icon>
|
|
||||||
<icon-location class="text-[var(--color-text-4)]" />
|
|
||||||
</template>
|
|
||||||
</a-button>
|
|
||||||
<MsSelect
|
|
||||||
v-model:model-value="checkedEnv"
|
|
||||||
mode="static"
|
|
||||||
:options="envOptions"
|
|
||||||
class="!w-[150px]"
|
|
||||||
:search-keys="['label']"
|
|
||||||
allow-search
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<a-input-search
|
<a-input-search
|
||||||
v-model:model-value="keyword"
|
v-model:model-value="keyword"
|
||||||
:placeholder="t('apiTestManagement.searchPlaceholder')"
|
:placeholder="t('apiTestManagement.searchPlaceholder')"
|
||||||
|
@ -251,7 +236,6 @@
|
||||||
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
|
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
|
||||||
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
|
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
|
||||||
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
|
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
|
||||||
import MsSelect from '@/components/business/ms-select';
|
|
||||||
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
|
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
|
||||||
import apiMethodSelect from '@/views/api-test/components/apiMethodSelect.vue';
|
import apiMethodSelect from '@/views/api-test/components/apiMethodSelect.vue';
|
||||||
import apiStatus from '@/views/api-test/components/apiStatus.vue';
|
import apiStatus from '@/views/api-test/components/apiStatus.vue';
|
||||||
|
@ -392,14 +376,6 @@
|
||||||
width: 150,
|
width: 150,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
if (!props.readOnly) {
|
|
||||||
const tableStore = useTableStore();
|
|
||||||
await tableStore.initColumn(TableKeyEnum.API_TEST, columns, 'drawer');
|
|
||||||
} else {
|
|
||||||
columns = columns.filter(
|
|
||||||
(item) => !['version', 'createTime', 'updateTime', 'operation'].includes(item.dataIndex as string)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(
|
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(
|
||||||
getDefinitionPage,
|
getDefinitionPage,
|
||||||
{
|
{
|
||||||
|
@ -758,6 +734,19 @@
|
||||||
function openApiTab(record: ApiDefinitionDetail) {
|
function openApiTab(record: ApiDefinitionDetail) {
|
||||||
emit('openApiTab', record);
|
emit('openApiTab', record);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
loadApiList,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!props.readOnly) {
|
||||||
|
const tableStore = useTableStore();
|
||||||
|
await tableStore.initColumn(TableKeyEnum.API_TEST, columns, 'drawer');
|
||||||
|
} else {
|
||||||
|
columns = columns.filter(
|
||||||
|
(item) => !['version', 'createTime', 'updateTime', 'operation'].includes(item.dataIndex as string)
|
||||||
|
);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
|
|
|
@ -1,15 +1,29 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex h-full flex-col">
|
<div class="flex h-full flex-col">
|
||||||
<div class="border-b border-[var(--color-text-n8)] px-[22px] pb-[16px]">
|
<div class="border-b border-[var(--color-text-n8)] px-[22px] pb-[16px]">
|
||||||
<MsEditableTab v-model:active-tab="activeApiTab" v-model:tabs="apiTabs" @add="addApiTab">
|
<MsEditableTab
|
||||||
|
v-model:active-tab="activeApiTab"
|
||||||
|
v-model:tabs="apiTabs"
|
||||||
|
@add="addApiTab"
|
||||||
|
@change="handleActiveTabChange"
|
||||||
|
>
|
||||||
<template #label="{ tab }">
|
<template #label="{ tab }">
|
||||||
<apiMethodName v-if="tab.id !== 'all'" :method="tab.method" class="mr-[4px]" />
|
<apiMethodName
|
||||||
{{ tab.name || tab.label }}
|
v-if="tab.id !== 'all'"
|
||||||
|
:method="tab.protocol === 'HTTP' ? tab.method : tab.protocol"
|
||||||
|
class="mr-[4px]"
|
||||||
|
/>
|
||||||
|
<a-tooltip :content="tab.name || tab.label" :mouse-enter-delay="500">
|
||||||
|
<div class="one-line-text max-w-[144px]">
|
||||||
|
{{ tab.name || tab.label }}
|
||||||
|
</div>
|
||||||
|
</a-tooltip>
|
||||||
</template>
|
</template>
|
||||||
</MsEditableTab>
|
</MsEditableTab>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="activeApiTab.id === 'all'" class="flex-1">
|
<div v-show="activeApiTab.id === 'all'" class="flex-1">
|
||||||
<apiTable
|
<apiTable
|
||||||
|
ref="apiTableRef"
|
||||||
:active-module="props.activeModule"
|
:active-module="props.activeModule"
|
||||||
:offspring-ids="props.offspringIds"
|
:offspring-ids="props.offspringIds"
|
||||||
:protocol="props.protocol"
|
:protocol="props.protocol"
|
||||||
|
@ -112,18 +126,48 @@
|
||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
<a-dropdown @select="handleSelect">
|
<div class="mb-[8px] flex items-center">
|
||||||
<a-button type="outline">
|
<div class="text-[var(--color-text-2)]">
|
||||||
<div class="flex items-center gap-[8px]">
|
{{ t('apiTestManagement.addDependency') }}
|
||||||
<icon-plus />
|
</div>
|
||||||
{{ t('apiTestManagement.addDependency') }}
|
<a-divider margin="4px" direction="vertical" />
|
||||||
|
<MsButton
|
||||||
|
type="text"
|
||||||
|
class="font-medium"
|
||||||
|
:disabled="activeApiTab.preDependency.length === 0 && activeApiTab.postDependency.length === 0"
|
||||||
|
@click="clearAllDependency"
|
||||||
|
>
|
||||||
|
{{ t('apiTestManagement.clearSelected') }}
|
||||||
|
</MsButton>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] p-[12px]">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="flex items-center gap-[4px] text-[var(--color-text-2)]">
|
||||||
|
{{ t('apiTestManagement.preDependency') }}
|
||||||
|
<div class="text-[rgb(var(--primary-5))]">
|
||||||
|
{{ activeApiTab.preDependency.length }}
|
||||||
|
</div>
|
||||||
|
{{ t('apiTestManagement.dependencyUnit') }}
|
||||||
</div>
|
</div>
|
||||||
</a-button>
|
<a-divider margin="8px" direction="vertical" />
|
||||||
<template #content>
|
<MsButton type="text" class="font-medium" @click="handleDddDependency('pre')">
|
||||||
<a-doption value="pre">{{ t('apiTestManagement.preDependency') }}</a-doption>
|
{{ t('apiTestManagement.addPreDependency') }}
|
||||||
<a-doption value="post">{{ t('apiTestManagement.postDependency') }}</a-doption>
|
</MsButton>
|
||||||
</template>
|
</div>
|
||||||
</a-dropdown>
|
<div class="mt-[8px] flex items-center">
|
||||||
|
<div class="flex items-center gap-[4px] text-[var(--color-text-2)]">
|
||||||
|
{{ t('apiTestManagement.postDependency') }}
|
||||||
|
<div class="text-[rgb(var(--primary-5))]">
|
||||||
|
{{ activeApiTab.postDependency.length }}
|
||||||
|
</div>
|
||||||
|
{{ t('apiTestManagement.dependencyUnit') }}
|
||||||
|
</div>
|
||||||
|
<a-divider margin="8px" direction="vertical" />
|
||||||
|
<MsButton type="text" class="font-medium" @click="handleDddDependency('post')">
|
||||||
|
{{ t('apiTestManagement.addPostDependency') }}
|
||||||
|
</MsButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</MsSplitBox>
|
</MsSplitBox>
|
||||||
|
@ -141,6 +185,7 @@
|
||||||
import { FormInstance, Message } from '@arco-design/web-vue';
|
import { FormInstance, Message } from '@arco-design/web-vue';
|
||||||
import { cloneDeep } from 'lodash-es';
|
import { cloneDeep } from 'lodash-es';
|
||||||
|
|
||||||
|
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||||
import MsEditableTab from '@/components/pure/ms-editable-tab/index.vue';
|
import MsEditableTab from '@/components/pure/ms-editable-tab/index.vue';
|
||||||
import { TabItem } from '@/components/pure/ms-editable-tab/types';
|
import { TabItem } from '@/components/pure/ms-editable-tab/types';
|
||||||
// import MsFormCreate from '@/components/pure/ms-form-create/formCreate.vue';
|
// import MsFormCreate from '@/components/pure/ms-form-create/formCreate.vue';
|
||||||
|
@ -188,13 +233,14 @@
|
||||||
);
|
);
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
allCount: number;
|
|
||||||
activeModule: string;
|
activeModule: string;
|
||||||
offspringIds: string[];
|
offspringIds: string[];
|
||||||
moduleTree: ModuleTreeNode[]; // 模块树
|
moduleTree: ModuleTreeNode[]; // 模块树
|
||||||
protocol: string;
|
protocol: string;
|
||||||
}>();
|
}>();
|
||||||
const emit = defineEmits(['addDone']);
|
const emit = defineEmits(['addDone']);
|
||||||
|
const setActiveApi: ((params: RequestParam) => void) | undefined = inject('setActiveApi');
|
||||||
|
const refreshModuleTree: (() => Promise<any>) | undefined = inject('refreshModuleTree');
|
||||||
|
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
@ -202,7 +248,7 @@
|
||||||
const apiTabs = ref<RequestParam[]>([
|
const apiTabs = ref<RequestParam[]>([
|
||||||
{
|
{
|
||||||
id: 'all',
|
id: 'all',
|
||||||
label: `${t('apiTestManagement.allApi')}(${props.allCount})`,
|
label: t('apiTestManagement.allApi'),
|
||||||
closable: false,
|
closable: false,
|
||||||
} as RequestParam,
|
} as RequestParam,
|
||||||
]);
|
]);
|
||||||
|
@ -214,7 +260,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const setActiveApi: ((params: RequestParam) => void) | undefined = inject('setActiveApi');
|
|
||||||
watch(
|
watch(
|
||||||
() => activeApiTab.value.id,
|
() => activeApiTab.value.id,
|
||||||
() => {
|
() => {
|
||||||
|
@ -338,6 +383,10 @@
|
||||||
response: cloneDeep(defaultResponse),
|
response: cloneDeep(defaultResponse),
|
||||||
responseDefinition: [cloneDeep(defaultResponseItem)],
|
responseDefinition: [cloneDeep(defaultResponseItem)],
|
||||||
isNew: true,
|
isNew: true,
|
||||||
|
mode: 'definition',
|
||||||
|
executeLoading: false,
|
||||||
|
preDependency: [], // 前置依赖
|
||||||
|
postDependency: [], // 后置依赖
|
||||||
};
|
};
|
||||||
|
|
||||||
function addApiTab(defaultProps?: Partial<TabItem>) {
|
function addApiTab(defaultProps?: Partial<TabItem>) {
|
||||||
|
@ -345,7 +394,7 @@
|
||||||
apiTabs.value.push({
|
apiTabs.value.push({
|
||||||
...cloneDeep(defaultDefinitionParams),
|
...cloneDeep(defaultDefinitionParams),
|
||||||
moduleId: props.activeModule === 'all' ? 'root' : props.activeModule,
|
moduleId: props.activeModule === 'all' ? 'root' : props.activeModule,
|
||||||
label: t('apiTestDebug.newApi'),
|
label: t('apiTestManagement.newApi'),
|
||||||
id,
|
id,
|
||||||
isNew: !defaultProps?.id, // 新开的tab标记为前端新增的调试,因为此时都已经有id了;但是如果是查看打开的会有携带id
|
isNew: !defaultProps?.id, // 新开的tab标记为前端新增的调试,因为此时都已经有id了;但是如果是查看打开的会有携带id
|
||||||
...defaultProps,
|
...defaultProps,
|
||||||
|
@ -353,6 +402,14 @@
|
||||||
activeApiTab.value = apiTabs.value[apiTabs.value.length - 1] as RequestParam;
|
activeApiTab.value = apiTabs.value[apiTabs.value.length - 1] as RequestParam;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const apiTableRef = ref<InstanceType<typeof apiTable>>();
|
||||||
|
|
||||||
|
function handleActiveTabChange(item: TabItem) {
|
||||||
|
if (item.id === 'all') {
|
||||||
|
apiTableRef.value?.loadApiList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
async function openApiTab(apiInfo: ModuleTreeNode | ApiDefinitionDetail) {
|
async function openApiTab(apiInfo: ModuleTreeNode | ApiDefinitionDetail) {
|
||||||
const isLoadedTabIndex = apiTabs.value.findIndex((e) => e.id === apiInfo.id);
|
const isLoadedTabIndex = apiTabs.value.findIndex((e) => e.id === apiInfo.id);
|
||||||
|
@ -403,7 +460,7 @@
|
||||||
const showAddDependencyDrawer = ref(false);
|
const showAddDependencyDrawer = ref(false);
|
||||||
const addDependencyMode = ref<'pre' | 'post'>('pre');
|
const addDependencyMode = ref<'pre' | 'post'>('pre');
|
||||||
|
|
||||||
function handleSelect(value: string | number | Record<string, any> | undefined) {
|
function handleDddDependency(value: string | number | Record<string, any> | undefined) {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case 'pre':
|
case 'pre':
|
||||||
addDependencyMode.value = 'pre';
|
addDependencyMode.value = 'pre';
|
||||||
|
@ -418,6 +475,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearAllDependency() {
|
||||||
|
activeApiTab.value.preDependency = [];
|
||||||
|
activeApiTab.value.postDependency = [];
|
||||||
|
}
|
||||||
|
|
||||||
const splitBoxRef = ref<InstanceType<typeof MsSplitBox>>();
|
const splitBoxRef = ref<InstanceType<typeof MsSplitBox>>();
|
||||||
const activeApiTabFormRef = ref<FormInstance>();
|
const activeApiTabFormRef = ref<FormInstance>();
|
||||||
|
|
||||||
|
@ -442,6 +504,9 @@
|
||||||
activeApiTab.value.name = res.name;
|
activeApiTab.value.name = res.name;
|
||||||
activeApiTab.value.label = res.name;
|
activeApiTab.value.label = res.name;
|
||||||
activeApiTab.value.url = res.path;
|
activeApiTab.value.url = res.path;
|
||||||
|
if (typeof refreshModuleTree === 'function') {
|
||||||
|
refreshModuleTree();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log(error);
|
console.log(error);
|
||||||
|
|
|
@ -5,14 +5,13 @@
|
||||||
ref="apiRef"
|
ref="apiRef"
|
||||||
:module-tree="props.moduleTree"
|
:module-tree="props.moduleTree"
|
||||||
:active-module="props.activeModule"
|
:active-module="props.activeModule"
|
||||||
:all-count="props.allCount"
|
|
||||||
:offspring-ids="props.offspringIds"
|
:offspring-ids="props.offspringIds"
|
||||||
:protocol="protocol"
|
:protocol="protocol"
|
||||||
/>
|
/>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
<a-tab-pane key="case" title="CASE" class="ms-api-tab-pane"> </a-tab-pane>
|
<a-tab-pane key="case" title="CASE" class="ms-api-tab-pane"> </a-tab-pane>
|
||||||
<a-tab-pane key="mock" title="MOCK" class="ms-api-tab-pane"> </a-tab-pane>
|
<a-tab-pane key="mock" title="MOCK" class="ms-api-tab-pane"> </a-tab-pane>
|
||||||
<a-tab-pane key="doc" :title="t('apiTestManagement.doc')" class="ms-api-tab-pane"> </a-tab-pane>
|
<!-- <a-tab-pane key="doc" title="API Docs" class="ms-api-tab-pane"> </a-tab-pane> -->
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<div class="flex items-center gap-[8px] pr-[24px]">
|
<div class="flex items-center gap-[8px] pr-[24px]">
|
||||||
<a-button type="outline" class="arco-btn-outline--secondary !p-[8px]">
|
<a-button type="outline" class="arco-btn-outline--secondary !p-[8px]">
|
||||||
|
@ -42,7 +41,6 @@
|
||||||
import { ModuleTreeNode } from '@/models/common';
|
import { ModuleTreeNode } from '@/models/common';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
allCount: number;
|
|
||||||
activeModule: string;
|
activeModule: string;
|
||||||
offspringIds: string[];
|
offspringIds: string[];
|
||||||
protocol: string;
|
protocol: string;
|
||||||
|
|
|
@ -526,9 +526,13 @@
|
||||||
initModuleCount();
|
initModuleCount();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function refresh() {
|
||||||
|
await initModules();
|
||||||
|
initModuleCount();
|
||||||
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
initModules,
|
refresh,
|
||||||
initModuleCount,
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
<template #first>
|
<template #first>
|
||||||
<div class="p-[24px]">
|
<div class="p-[24px]">
|
||||||
<moduleTree
|
<moduleTree
|
||||||
|
ref="moduleTreeRef"
|
||||||
:active-node-id="activeApi?.id"
|
:active-node-id="activeApi?.id"
|
||||||
@init="handleModuleInit"
|
@init="handleModuleInit"
|
||||||
@new-api="newApi"
|
@new-api="newApi"
|
||||||
|
@ -40,7 +41,6 @@
|
||||||
<management
|
<management
|
||||||
ref="managementRef"
|
ref="managementRef"
|
||||||
:module-tree="folderTree"
|
:module-tree="folderTree"
|
||||||
:all-count="allCount"
|
|
||||||
:active-module="activeModule"
|
:active-module="activeModule"
|
||||||
:offspring-ids="offspringIds"
|
:offspring-ids="offspringIds"
|
||||||
:protocol="protocol"
|
:protocol="protocol"
|
||||||
|
@ -65,11 +65,11 @@
|
||||||
|
|
||||||
const activeModule = ref<string>('all');
|
const activeModule = ref<string>('all');
|
||||||
const folderTree = ref<ModuleTreeNode[]>([]);
|
const folderTree = ref<ModuleTreeNode[]>([]);
|
||||||
const allCount = ref(0);
|
|
||||||
const importDrawerVisible = ref(false);
|
const importDrawerVisible = ref(false);
|
||||||
const offspringIds = ref<string[]>([]);
|
const offspringIds = ref<string[]>([]);
|
||||||
const protocol = ref('HTTP');
|
const protocol = ref('HTTP');
|
||||||
const activeApi = ref<RequestParam>();
|
const activeApi = ref<RequestParam>();
|
||||||
|
const moduleTreeRef = ref<InstanceType<typeof moduleTree>>();
|
||||||
const managementRef = ref<InstanceType<typeof management>>();
|
const managementRef = ref<InstanceType<typeof management>>();
|
||||||
|
|
||||||
function handleModuleInit(tree, _protocol: string) {
|
function handleModuleInit(tree, _protocol: string) {
|
||||||
|
@ -98,7 +98,13 @@
|
||||||
protocol.value = val;
|
protocol.value = val;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function refreshModuleTree() {
|
||||||
|
moduleTreeRef.value?.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 向子孙组件提供方法 */
|
||||||
provide('setActiveApi', setActiveApi);
|
provide('setActiveApi', setActiveApi);
|
||||||
|
provide('refreshModuleTree', refreshModuleTree);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped></style>
|
<style lang="less" scoped></style>
|
||||||
|
|
|
@ -1 +1,103 @@
|
||||||
export default {};
|
export default {
|
||||||
|
'apiTestManagement.newApi': 'Create api',
|
||||||
|
'apiTestManagement.importApi': 'Import api',
|
||||||
|
'apiTestManagement.fileImport': 'Import file',
|
||||||
|
'apiTestManagement.timeImport': 'Scheduled import',
|
||||||
|
'apiTestManagement.addSubModule': 'Add submodule',
|
||||||
|
'apiTestManagement.allApi': 'All api',
|
||||||
|
'apiTestManagement.searchTip': 'Please enter module/api name',
|
||||||
|
'apiTestManagement.moveSearchTip': 'Please enter the module name to search',
|
||||||
|
'apiTestManagement.noMatchModule': 'No matching module/api yet',
|
||||||
|
'apiTestManagement.execute': 'Execute',
|
||||||
|
'apiTestManagement.share': 'Share API',
|
||||||
|
'apiTestManagement.shareModule': 'Share module',
|
||||||
|
'apiTestManagement.doc': 'Document',
|
||||||
|
'apiTestManagement.closeAll': 'Close all tabs',
|
||||||
|
'apiTestManagement.closeOther': 'Close other tabs',
|
||||||
|
'apiTestManagement.showSubdirectory': 'Show subdirectory use case',
|
||||||
|
'apiTestManagement.searchPlaceholder': 'Enter ID/name/api path search',
|
||||||
|
'apiTestManagement.apiName': 'Api name',
|
||||||
|
'apiTestManagement.apiType': 'Api type',
|
||||||
|
'apiTestManagement.apiStatus': 'Status',
|
||||||
|
'apiTestManagement.path': 'Path',
|
||||||
|
'apiTestManagement.version': 'Version',
|
||||||
|
'apiTestManagement.createTime': 'Creation time',
|
||||||
|
'apiTestManagement.updateTime': 'Update time',
|
||||||
|
'apiTestManagement.deprecate': 'Deprecated',
|
||||||
|
'apiTestManagement.processing': 'Processing',
|
||||||
|
'apiTestManagement.debugging': 'Debugging',
|
||||||
|
'apiTestManagement.done': 'Completed',
|
||||||
|
'apiTestManagement.deleteApiTipTitle': 'Are you sure you want to delete {name}?',
|
||||||
|
'apiTestManagement.deleteApiTip':
|
||||||
|
'After deletion, the interface will be placed in the recycle bin, where data recovery can be performed',
|
||||||
|
'apiTestManagement.batchDeleteApiTip': 'Are you sure you want to delete {count} selected interfaces?',
|
||||||
|
'apiTestManagement.batchModalSubTitle': '({count} interfaces selected)',
|
||||||
|
'apiTestManagement.chooseAttr': 'Select properties',
|
||||||
|
'apiTestManagement.attrRequired': 'Property cannot be empty',
|
||||||
|
'apiTestManagement.batchUpdate': 'Batch update to',
|
||||||
|
'apiTestManagement.valueRequired': 'Attribute value cannot be empty',
|
||||||
|
'apiTestManagement.batchMoveConfirm': 'Move to selected module',
|
||||||
|
'apiTestManagement.belongModule': 'Belonging module',
|
||||||
|
'apiTestManagement.importMode': 'Import mode',
|
||||||
|
'apiTestManagement.importModeTip1': 'Cover:',
|
||||||
|
'apiTestManagement.importModeTip2':
|
||||||
|
'1.If the request type + path is the same for the same interface, if the request parameter content is inconsistent, it will be overwritten.',
|
||||||
|
'apiTestManagement.importModeTip3':
|
||||||
|
'2.The same interface request type + path are the same, and the request parameter content is the same and does not change.',
|
||||||
|
'apiTestManagement.importModeTip4':
|
||||||
|
'3.If the interface is not the same and the request type + path is the same, add it',
|
||||||
|
'apiTestManagement.importModeTip5': 'Not covered:',
|
||||||
|
'apiTestManagement.importModeTip6':
|
||||||
|
'1.If the same interface request type + path are the same, no changes will be made.',
|
||||||
|
'apiTestManagement.importModeTip7': '2.If the request type + path are not the same for the same interface, add a new',
|
||||||
|
'apiTestManagement.cover': 'Cover',
|
||||||
|
'apiTestManagement.uncover': 'Not covered',
|
||||||
|
'apiTestManagement.moreSetting': 'More settings',
|
||||||
|
'apiTestManagement.importType': 'Import method',
|
||||||
|
'apiTestManagement.urlImport': 'URL import',
|
||||||
|
'apiTestManagement.syncImportCase': 'Synchronous import interface use case',
|
||||||
|
'apiTestManagement.syncUpdateDirectory': 'Synchronously update the directory where the interface is located',
|
||||||
|
'apiTestManagement.importSwaggerFileTip1': 'Supports json files of Swagger 3.0 version,',
|
||||||
|
'apiTestManagement.importSwaggerFileTip2':
|
||||||
|
'It is recommended to convert 2.0 files to 3.0 on the official website and then import them.',
|
||||||
|
'apiTestManagement.importSwaggerFileTip3': ', the size does not exceed 50M',
|
||||||
|
'apiTestManagement.urlImportPlaceholder': 'Please enter the OpenAPI/Swagger URL',
|
||||||
|
'apiTestManagement.swaggerURLRequired': 'SwaggerURL cannot be empty',
|
||||||
|
'apiTestManagement.basicAuth': 'Basic Authentication',
|
||||||
|
'apiTestManagement.account': 'Account',
|
||||||
|
'apiTestManagement.accountRequired': 'Account cannot be empty',
|
||||||
|
'apiTestManagement.password': 'Password',
|
||||||
|
'apiTestManagement.passwordRequired': 'Password can not be blank',
|
||||||
|
'apiTestManagement.taskName': 'Task name',
|
||||||
|
'apiTestManagement.taskNamePlaceholder': 'Please enter a task name',
|
||||||
|
'apiTestManagement.taskNameRequired': 'Task name cannot be empty',
|
||||||
|
'apiTestManagement.syncFrequency': 'Sync frequency',
|
||||||
|
'apiTestManagement.timeTaskList': 'Scheduled task list',
|
||||||
|
'apiTestManagement.timeTaskHour': '(per hour)',
|
||||||
|
'apiTestManagement.timeTaskSixHour': '(every 6 hours)',
|
||||||
|
'apiTestManagement.timeTaskTwelveHour': '(every 12 hours)',
|
||||||
|
'apiTestManagement.timeTaskDay': '(every day)',
|
||||||
|
'apiTestManagement.customFrequency': 'Custom frequency',
|
||||||
|
'apiTestManagement.case': 'Case',
|
||||||
|
'apiTestManagement.definition': 'Definition',
|
||||||
|
'apiTestManagement.debug': 'Debug',
|
||||||
|
'apiTestManagement.addDependency': 'Select dependency use case',
|
||||||
|
'apiTestManagement.clearSelected': 'Clear selected use cases',
|
||||||
|
'apiTestManagement.preDependency': 'Front interface',
|
||||||
|
'apiTestManagement.addPreDependency': 'Add pre-dependency',
|
||||||
|
'apiTestManagement.postDependency': 'Rear interface',
|
||||||
|
'apiTestManagement.addPostDependency': 'Add post-dependency',
|
||||||
|
'apiTestManagement.dependencyUnit': 'item',
|
||||||
|
'apiTestManagement.saveAsCase': 'Save as new use case',
|
||||||
|
'apiTestManagement.apiNamePlaceholder': 'Please enter the interface name',
|
||||||
|
'apiTestManagement.executeResult': 'Execution result',
|
||||||
|
'apiTestManagement.setDefault': 'Set as Default',
|
||||||
|
'apiTestManagement.confirmDelete': 'Are you sure you want to delete {name}?',
|
||||||
|
'apiTestManagement.response': 'Response{count}',
|
||||||
|
'apiTestManagement.responseCode': 'Response code',
|
||||||
|
'apiTestManagement.dynamicConversion': 'Dynamic conversion',
|
||||||
|
'apiTestManagement.expandApi': 'Show all requests',
|
||||||
|
'apiTestManagement.collapseApi': 'Hide all requests',
|
||||||
|
'apiTestManagement.paramName': 'Parameter name',
|
||||||
|
'apiTestManagement.paramVal': 'Parameter value',
|
||||||
|
};
|
||||||
|
|
|
@ -19,7 +19,6 @@ export default {
|
||||||
'apiTestManagement.apiName': '接口名称',
|
'apiTestManagement.apiName': '接口名称',
|
||||||
'apiTestManagement.apiType': '请求类型',
|
'apiTestManagement.apiType': '请求类型',
|
||||||
'apiTestManagement.apiStatus': '状态',
|
'apiTestManagement.apiStatus': '状态',
|
||||||
'apiTestManagement.responsiblePerson': '责任人',
|
|
||||||
'apiTestManagement.path': '路径',
|
'apiTestManagement.path': '路径',
|
||||||
'apiTestManagement.version': '版本',
|
'apiTestManagement.version': '版本',
|
||||||
'apiTestManagement.createTime': '创建时间',
|
'apiTestManagement.createTime': '创建时间',
|
||||||
|
@ -75,11 +74,14 @@ export default {
|
||||||
'apiTestManagement.customFrequency': '自定义频率',
|
'apiTestManagement.customFrequency': '自定义频率',
|
||||||
'apiTestManagement.case': '用例',
|
'apiTestManagement.case': '用例',
|
||||||
'apiTestManagement.definition': '定义',
|
'apiTestManagement.definition': '定义',
|
||||||
'apiTestManagement.addDependency': '添加依赖关系',
|
'apiTestManagement.debug': '调试',
|
||||||
'apiTestManagement.preDependency': '前置依赖',
|
'apiTestManagement.addDependency': '选择依赖关系用例',
|
||||||
|
'apiTestManagement.clearSelected': '清空已选用例',
|
||||||
|
'apiTestManagement.preDependency': '前置接口',
|
||||||
'apiTestManagement.addPreDependency': '添加前置依赖',
|
'apiTestManagement.addPreDependency': '添加前置依赖',
|
||||||
'apiTestManagement.postDependency': '后置依赖',
|
'apiTestManagement.postDependency': '后置接口',
|
||||||
'apiTestManagement.addPostDependency': '添加后置依赖',
|
'apiTestManagement.addPostDependency': '添加后置依赖',
|
||||||
|
'apiTestManagement.dependencyUnit': '条',
|
||||||
'apiTestManagement.saveAsCase': '保存为新用例',
|
'apiTestManagement.saveAsCase': '保存为新用例',
|
||||||
'apiTestManagement.apiNamePlaceholder': '请输入接口名称',
|
'apiTestManagement.apiNamePlaceholder': '请输入接口名称',
|
||||||
'apiTestManagement.executeResult': '执行结果',
|
'apiTestManagement.executeResult': '执行结果',
|
||||||
|
|
Loading…
Reference in New Issue