fix(场景步骤): 场景交互优化

This commit is contained in:
baiqi 2024-03-29 10:32:29 +08:00 committed by Craftsman
parent 6bccc40e9c
commit c6ee982ab8
19 changed files with 133 additions and 48 deletions

View File

@ -332,14 +332,14 @@
</paramsTable>
</div>
<!-- 正则结束 -->
<fastExtraction
v-model:visible="fastExtractionVisible"
:config="activeRecord"
:response="props.response"
:is-show-more-setting="false"
@apply="handleFastExtractionApply"
/>
</div>
<fastExtraction
v-model:visible="fastExtractionVisible"
:config="activeRecord"
:response="props.response"
:is-show-more-setting="false"
@apply="handleFastExtractionApply"
/>
</template>
<script setup lang="ts">

View File

@ -257,15 +257,13 @@
formalParameterVars,
JMeterAllGroup,
JMeterAllVars,
JMeterVariableGroup,
mockAllGroup,
mockAllParams,
mockFunctions,
sameFuncNameVars,
} from './config';
import type { MockParamInputGroupItem, MockParamItem } from './types';
import type { AutoComplete, CascaderOption, FormInstance } from '@arco-design/web-vue';
import { string } from 'fast-glob/out/utils';
import type { AutoComplete, FormInstance } from '@arco-design/web-vue';
const props = defineProps<{
value: string;

View File

@ -1,6 +1,6 @@
<template>
<a-tooltip :content="tagsTooltip">
<div class="flex max-w-[440px] flex-row">
<div class="flex max-w-[440px] flex-row" @click="emit('click')">
<MsTag v-for="tag of showTagList" :key="tag.id" :width="getTagWidth(tag)" :size="props.size" v-bind="attrs">
{{ props.isStringTag ? tag : tag[props.nameKey] }}
</MsTag>
@ -30,6 +30,9 @@
size: 'medium',
}
);
const emit = defineEmits<{
(e: 'click'): void;
}>();
const attrs = useAttrs();

View File

@ -12,6 +12,7 @@
:readonly="props.readonly"
:class="props.inputClass"
:size="props.size"
:disabled="props.disabled"
@press-enter="tagInputEnter"
@blur="tagInputBlur"
@clear="emit('clear')"
@ -53,6 +54,7 @@
class?: string;
inputClass?: string;
size?: 'small' | 'large' | 'medium' | 'mini';
disabled?: boolean;
}>(),
{
retainInputValue: true,

View File

@ -310,7 +310,6 @@ export type ScenarioStepDetail = Partial<
ScenarioStepConfig & {
protocol: string;
method: RequestMethods;
isRefScenarioStep?: boolean; // 是否是完全引用的场景下的步骤,是的话不允许启用禁用
}
>;
export interface ScenarioStepItem {
@ -340,6 +339,8 @@ export interface ScenarioStepItem {
executeStatus?: ScenarioExecuteStatus;
isExecuting?: boolean; // 是否正在执行
reportId?: string | number; // 步骤单个调试时的报告id
isQuoteScenarioStep?: boolean; // 是否是引用场景下的步骤(不分是不是完全引用,只要是引用类型就是),不可修改引用 api 的参数值
isRefScenarioStep?: boolean; // 是否是完全引用的场景下的步骤,是的话不允许启用禁用
}
// 场景
export interface Scenario {

View File

@ -546,7 +546,6 @@
import { groupCategoryEnvList, groupProjectEnv } from '@/api/modules/project-management/envManagement';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { isArray, isEmptyObject, isObject, isString } from '@/utils/is';
import { ModuleTreeNode, TransferFileParams } from '@/models/common';
import { HttpForm, ProjectOptionItem } from '@/models/projectManagement/environmental';

View File

@ -1,5 +1,10 @@
<template>
<assertion v-model:params="assertionConfig.assertions" :is-definition="false" :assertion-config="assertionConfig" />
<assertion
v-model:params="assertionConfig.assertions"
:is-definition="false"
:assertion-config="assertionConfig"
@change="emit('change')"
/>
</template>
<script setup lang="ts">
@ -7,6 +12,10 @@
import { AssertionConfig } from '@/models/apiTest/scenario';
const emit = defineEmits<{
(e: 'change'): void;
}>();
const assertionConfig = defineModel<AssertionConfig>('assertionConfig', {
required: true,
});

View File

@ -125,7 +125,7 @@
v-model:model-value="requestVModel.name"
:max-length="255"
:placeholder="t('apiTestManagement.apiNamePlaceholder')"
:disabled="!isEditableApi"
:disabled="!isEditableApi || isQuoteScenarioStep"
allow-clear
class="mt-[8px]"
/>
@ -179,6 +179,7 @@
<httpHeader
v-if="requestVModel.activeTab === RequestComposition.HEADER"
v-model:params="requestVModel.headers"
:disabled-param-value="isQuoteScenarioStep"
:disabled-except-param="!isEditableApi"
:layout="activeLayout"
:second-box-height="secondBoxHeight"
@ -188,6 +189,7 @@
v-else-if="requestVModel.activeTab === RequestComposition.BODY"
v-model:params="requestVModel.body"
:layout="activeLayout"
:disabled-param-value="isQuoteScenarioStep"
:disabled-except-param="!isEditableApi"
:second-box-height="secondBoxHeight"
:upload-temp-file-api="uploadTempFile"
@ -200,6 +202,7 @@
v-else-if="requestVModel.activeTab === RequestComposition.QUERY"
v-model:params="requestVModel.query"
:layout="activeLayout"
:disabled-param-value="isQuoteScenarioStep"
:disabled-except-param="!isEditableApi"
:second-box-height="secondBoxHeight"
@change="handleActiveDebugChange"
@ -208,6 +211,7 @@
v-else-if="requestVModel.activeTab === RequestComposition.REST"
v-model:params="requestVModel.rest"
:layout="activeLayout"
:disabled-param-value="isQuoteScenarioStep"
:disabled-except-param="!isEditableApi"
:second-box-height="secondBoxHeight"
@change="handleActiveDebugChange"
@ -216,7 +220,7 @@
v-else-if="requestVModel.activeTab === RequestComposition.PRECONDITION"
v-model:config="requestVModel.children[0].preProcessorConfig"
is-definition
:disabled="!isEditableApi"
:disabled="!isEditableApi || isQuoteScenarioStep"
@change="handleActiveDebugChange"
/>
<postcondition
@ -224,7 +228,7 @@
v-model:config="requestVModel.children[0].postProcessorConfig"
:response="props.stepResponses?.[requestVModel.stepId]?.responseResult.body"
:layout="activeLayout"
:disabled="!isEditableApi"
:disabled="!isEditableApi || isQuoteScenarioStep"
:second-box-height="secondBoxHeight"
is-definition
@change="handleActiveDebugChange"
@ -234,19 +238,19 @@
v-model:params="requestVModel.children[0].assertionConfig.assertions"
:response="props.stepResponses?.[requestVModel.stepId]?.responseResult.body"
is-definition
:disabled="!isEditableApi"
:disabled="!isEditableApi || isQuoteScenarioStep"
:assertion-config="requestVModel.children[0].assertionConfig"
/>
<auth
v-else-if="requestVModel.activeTab === RequestComposition.AUTH"
v-model:params="requestVModel.authConfig"
:disabled="!isEditableApi"
:disabled="!isEditableApi || isQuoteScenarioStep"
@change="handleActiveDebugChange"
/>
<setting
v-else-if="requestVModel.activeTab === RequestComposition.SETTING"
v-model:params="requestVModel.otherConfig"
:disabled="!isEditableApi"
:disabled="!isEditableApi || isQuoteScenarioStep"
@change="handleActiveDebugChange"
/>
</div>
@ -501,6 +505,8 @@
() => _stepType.value.isCopyApi || props.step?.stepType === ScenarioStepType.CUSTOM_REQUEST || !props.step
);
const isHttpProtocol = computed(() => requestVModel.value.protocol === 'HTTP');
const isQuoteScenarioStep = computed(() => props.step?.isQuoteScenarioStep);
const isInitPluginForm = ref(false);
function handleActiveDebugChange() {
@ -945,6 +951,7 @@
protocol: requestVModel.value.protocol,
method: isHttpProtocol.value ? requestVModel.value.method : requestVModel.value.protocol,
name: requestVModel.value.name,
unSaved: requestVModel.value.unSaved,
customizeRequest: props.step?.stepType === ScenarioStepType.CUSTOM_REQUEST || !props.request,
customizeRequestEnvEnable: requestVModel.value.customizeRequestEnvEnable,
children: [
@ -1031,7 +1038,8 @@
responseActiveTab: ResponseComposition.BODY,
...parseRequestBodyResult,
};
if (_stepType.value.isQuoteApi && props.request && isHttpProtocol.value) {
if (_stepType.value.isQuoteApi && props.request && isHttpProtocol.value && !props.step?.isQuoteScenarioStep) {
// api
// queryrest
['headers', 'query', 'rest'].forEach((type) => {
props.request?.[type]?.forEach((item) => {

View File

@ -260,11 +260,9 @@
return {
...node,
copyFromStepId: node.id,
config: {
...node.config,
isRefScenarioStep: true, //
},
id: getGenerateId(),
isQuoteScenarioStep: true,
isRefScenarioStep: true, //
};
}),
};

View File

@ -69,12 +69,13 @@ export const defaultStepItemCommon = {
id: '',
name: '',
enable: true,
isRefScenarioStep: false,
},
createActionsVisible: false,
responsePopoverVisible: false,
isExecuting: false,
executeStatus: undefined,
isRefScenarioStep: false,
isQuoteScenarioStep: false,
};
export const defaultScenario: Scenario = {

View File

@ -1,11 +1,16 @@
<template>
<div class="condition">
<div>
<precondition v-model:config="preProcessorConfig" :is-definition="false" />
<precondition v-model:config="preProcessorConfig" :is-definition="false" @change="emit('change')" />
</div>
<a-divider class="my-[8px]" type="dashed" />
<div>
<postcondition v-model:config="postProcessorConfig" :is-definition="false" :layout="activeLayout" />
<postcondition
v-model:config="postProcessorConfig"
:is-definition="false"
:layout="activeLayout"
@change="emit('change')"
/>
</div>
</div>
</template>
@ -16,6 +21,10 @@
import { ExecuteConditionConfig } from '@/models/apiTest/common';
const emit = defineEmits<{
(e: 'change');
}>();
const activeLayout = ref<'horizontal' | 'vertical'>('vertical');
const preProcessorConfig = defineModel<ExecuteConditionConfig>('preProcessorConfig', {
required: true,

View File

@ -12,11 +12,11 @@
</a-tooltip>
</div>
<div class="mb-[16px] mt-[10px] flex items-center gap-[8px]">
<a-switch v-model:model-value="form.enableGlobalCookie" type="line" size="small" />
<a-switch v-model:model-value="form.enableGlobalCookie" type="line" size="small" @change="emit('change')" />
{{ t('apiScenario.setting.environment.cookie') }}
</div>
<div class="mb-[16px] flex items-center gap-[8px]">
<a-switch v-model:model-value="form.enableCookieShare" type="line" size="small" />
<a-switch v-model:model-value="form.enableCookieShare" type="line" size="small" @change="emit('change')" />
{{ t('apiScenario.setting.share.cookie') }}
<a-tooltip :content="t('apiScenario.setting.share.cookie.tip')" position="right">
<div>
@ -31,7 +31,7 @@
{{ t('apiScenario.setting.run.config') }}
</div>
<div class="mb-[16px] mt-[10px] flex items-center gap-[8px]">
<a-switch v-model:model-value="form.enableStepWait" type="line" size="small" />
<a-switch v-model:model-value="form.enableStepWait" type="line" size="small" @change="emit('change')" />
{{ t('apiScenario.setting.step.waitTime') }}
<a-tooltip :content="t('apiScenario.setting.waitTime.tip')">
<div>
@ -50,7 +50,14 @@
<div class="text-[var(--color-text-brand)]">(ms)</div>
</div>
</template>
<a-input-number v-model:model-value="form.stepWaitTime" mode="button" :step="100" :min="0" class="w-[160px]" />
<a-input-number
v-model:model-value="form.stepWaitTime"
mode="button"
:step="100"
:min="0"
class="w-[160px]"
@change="emit('change')"
/>
</a-form-item>
<a-form-item class="flex-1">
@ -59,7 +66,7 @@
{{ t('apiScenario.setting.step.rule') }}
</div>
</template>
<a-radio-group v-model:model-value="form.failureStrategy">
<a-radio-group v-model:model-value="form.failureStrategy" @change="emit('change')">
<a-radio :value="ScenarioFailureStrategy.CONTINUE">{{ t('apiScenario.setting.step.rule.ignore') }}</a-radio>
<a-radio :value="ScenarioFailureStrategy.STOP">{{ t('apiScenario.setting.step.rule.stop') }}</a-radio>
</a-radio-group>
@ -73,6 +80,10 @@
import { OtherConfig } from '@/models/apiTest/scenario';
import { ScenarioFailureStrategy } from '@/enums/apiEnum';
const emit = defineEmits<{
(e: 'change'): void;
}>();
const { t } = useI18n();
const form = defineModel<OtherConfig>('otherConfig', {

View File

@ -54,11 +54,7 @@
import { ScenarioAddStepActionType, ScenarioStepRefType, ScenarioStepType } from '@/enums/apiEnum';
import useCreateActions from './useCreateActions';
import {
defaultConditionController,
defaultLoopController,
defaultStepItemCommon,
} from '@/views/api-test/scenario/components/config';
import { defaultStepItemCommon } from '@/views/api-test/scenario/components/config';
import { DropdownPosition } from '@arco-design/web-vue/es/dropdown/interface';
const props = defineProps<{
@ -76,6 +72,7 @@
| ScenarioAddStepActionType.SCRIPT_OPERATION,
step?: ScenarioStepItem
);
(e: 'addDone');
}>();
const appStore = useAppStore();
@ -126,6 +123,7 @@
)[0]
);
}
emit('addDone');
break;
case ScenarioAddStepActionType.CONDITION_CONTROL:
if (step.value && props.createStepAction) {
@ -151,6 +149,7 @@
)[0]
);
}
emit('addDone');
break;
case ScenarioAddStepActionType.ONLY_ONCE_CONTROL:
if (step.value && props.createStepAction) {
@ -176,6 +175,7 @@
)[0]
);
}
emit('addDone');
break;
case ScenarioAddStepActionType.WAIT_TIME:
if (step.value && props.createStepAction) {
@ -201,6 +201,7 @@
)[0]
);
}
emit('addDone');
break;
case ScenarioAddStepActionType.IMPORT_SYSTEM_API:
case ScenarioAddStepActionType.CUSTOM_API:

View File

@ -20,6 +20,7 @@
:popup-translate="[-7, -10]"
@other-create="(type, step) => emit('otherCreate', type, step, activeCreateAction)"
@close="handleActionsClose"
@add-done="emit('addDone')"
>
<span></span>
</createStepActions>
@ -85,6 +86,7 @@
step?: ScenarioStepItem,
activeCreateAction?: CreateStepAction
);
(e: 'addDone');
}>();
const { t } = useI18n();

View File

@ -135,8 +135,9 @@ export default function useCreateActions() {
config: {
...defaultStepItemCommon.config,
...config,
isRefScenarioStep: item.config?.isRefScenarioStep || false,
},
isQuoteScenarioStep: item.config?.isQuoteScenarioStep || false,
isRefScenarioStep: item.config?.isRefScenarioStep || false,
children: item.children || [],
stepType,
refType,

View File

@ -229,7 +229,7 @@
});
} else {
scenario.value.steps = mapTree(scenario.value.steps, (node) => {
if (ids.has(node.id) && node.config.isRefScenarioStep !== true) {
if (ids.has(node.id) && node.isRefScenarioStep !== true) {
//
node.enable = isBatchEnable.value;
}

View File

@ -60,7 +60,7 @@
<div class="mr-[8px] flex items-center gap-[8px]">
<!-- 步骤启用/禁用完全引用的场景下的子孙步骤不可禁用 -->
<a-switch
v-show="step.config.isRefScenarioStep !== true"
v-show="step.isRefScenarioStep !== true"
v-model:model-value="step.enable"
size="small"
@click.stop="handleStepToggleEnable(step)"
@ -174,6 +174,7 @@
@click="setFocusNodeKey(step.id)"
@other-create="handleOtherCreate"
@close="setFocusNodeKey('')"
@add-done="scenario.unSaved = true"
/>
</template>
<template #extraEnd="step">
@ -224,7 +225,12 @@
</template>
</MsTree>
</a-spin>
<createStepActions v-model:selected-keys="selectedKeys" v-model:steps="steps" @other-create="handleOtherCreate">
<createStepActions
v-model:selected-keys="selectedKeys"
v-model:steps="steps"
@add-done="scenario.unSaved = true"
@other-create="handleOtherCreate"
>
<a-button type="dashed" class="add-step-btn" long>
<div class="flex items-center gap-[8px]">
<icon-plus />
@ -716,14 +722,15 @@
realStep.children = mapTree<ScenarioStepItem>(realStep.children || [], (child) => {
//
if (scenarioConfigForm.value.refType === ScenarioStepRefType.REF) {
child.config.isRefScenarioStep = true;
child.isRefScenarioStep = true;
child.enable = true;
} else {
child.config.isRefScenarioStep = false;
child.isRefScenarioStep = false;
}
return child;
});
Message.success(t('apiScenario.setSuccess'));
scenario.value.unSaved = true;
cancelScenarioConfig();
}
}
@ -765,6 +772,7 @@
}
return {
...cloneDeep(childNode),
executeStatus: undefined,
copyFromStepId: childCopyFromStepId,
id: childId,
};
@ -780,6 +788,7 @@
checkedIfNeed,
'id'
);
scenario.value.unSaved = true;
break;
case 'config':
activeStep.value = node as ScenarioStepItem;
@ -791,6 +800,7 @@
break;
case 'delete':
deleteNode(steps.value, node.id, 'id');
scenario.value.unSaved = true;
break;
default:
break;
@ -802,7 +812,7 @@
}
/**
* 处理步骤名称编辑
* 处理apicase场景步骤名称编辑
*/
const showStepNameEditInputStepId = ref<string | number>('');
const tempStepName = ref('');
@ -822,10 +832,11 @@
realStep.name = tempStepName.value;
}
showStepNameEditInputStepId.value = '';
scenario.value.unSaved = true;
}
/**
* 处理步骤名称编辑
* 处理apicase场景步骤名称编辑
*/
const showStepDescEditInputStepId = ref<string | number>('');
const tempStepDesc = ref('');
@ -845,6 +856,7 @@
realStep.name = tempStepDesc.value;
}
showStepDescEditInputStepId.value = '';
scenario.value.unSaved = true;
}
function handleStepContentChange($event, step: ScenarioStepItem) {
@ -853,6 +865,7 @@
Object.keys($event).forEach((key) => {
realStep.config[key] = $event[key];
});
scenario.value.unSaved = true;
}
}
@ -870,6 +883,7 @@
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, data.id, 'id');
if (realStep) {
realStep.enable = !realStep.enable;
scenario.value.unSaved = true;
}
}
@ -1218,6 +1232,7 @@
} else {
steps.value = steps.value.concat(insertSteps);
}
scenario.value.unSaved = true;
}
/**
@ -1260,12 +1275,16 @@
projectId: appStore.currentProjectId,
});
}
scenario.value.unSaved = true;
}
/**
* API 详情抽屉关闭时应用更改
*/
function applyApiStep(request: RequestParam | CaseRequestParam) {
if (request.unSaved) {
scenario.value.unSaved = true;
}
if (activeStep.value) {
request.isNew = false;
stepDetails.value[activeStep.value?.id] = request;
@ -1282,6 +1301,7 @@
customCaseDrawerVisible.value = false;
deleteNode(steps.value, step.id, 'id');
activeStep.value = undefined;
scenario.value.unSaved = true;
}
}
@ -1314,6 +1334,7 @@
projectId: appStore.currentProjectId,
});
}
scenario.value.unSaved = true;
}
/**
@ -1383,6 +1404,7 @@
const dragResult = handleTreeDragDrop(steps.value, dragNode, dropNode, dropPosition, 'id');
if (dragResult) {
Message.success(t('common.moveSuccess'));
scenario.value.unSaved = true;
}
} catch (error) {
// eslint-disable-next-line no-console
@ -1432,6 +1454,7 @@
}
showQuickInput.value = false;
clearQuickInput();
scenario.value.unSaved = true;
}
}

View File

@ -53,6 +53,7 @@
<params
v-if="activeKey === ScenarioDetailComposition.PARAMS"
v-model:params="scenario.scenarioConfig.variable.commonVariables"
@change="scenario.unSaved = true"
/>
</a-tab-pane>
<a-tab-pane
@ -64,6 +65,7 @@
v-if="activeKey === ScenarioDetailComposition.PRE_POST"
v-model:post-processor-config="scenario.scenarioConfig.postProcessorConfig"
v-model:pre-processor-config="scenario.scenarioConfig.preProcessorConfig"
@change="scenario.unSaved = true"
/>
</a-tab-pane>
<a-tab-pane
@ -74,6 +76,7 @@
<assertion
v-if="activeKey === ScenarioDetailComposition.ASSERTION"
v-model:assertion-config="scenario.scenarioConfig.assertionConfig"
@change="scenario.unSaved = true"
/>
</a-tab-pane>
<a-tab-pane
@ -108,6 +111,7 @@
<setting
v-if="activeKey === ScenarioDetailComposition.SETTING"
v-model:other-config="scenario.scenarioConfig.otherConfig"
@change="scenario.unSaved = true"
/>
</a-tab-pane>
</a-tabs>

View File

@ -129,7 +129,7 @@
} from '@/models/apiTest/scenario';
import { ModuleTreeNode } from '@/models/common';
import { EnvConfig } from '@/models/projectManagement/environmental';
import { ScenarioExecuteStatus, ScenarioStepType } from '@/enums/apiEnum';
import { ScenarioExecuteStatus, ScenarioStepRefType, ScenarioStepType } from '@/enums/apiEnum';
import { ApiTestRouteEnum } from '@/enums/routeEnum';
import { defaultScenario } from './components/config';
@ -371,7 +371,22 @@
};
});
} else {
copySteps = mapTree(defaultScenarioInfo.steps);
copySteps = mapTree(defaultScenarioInfo.steps, (node) => {
if (
node.parent &&
node.parent.stepType === ScenarioStepType.API_SCENARIO &&
[ScenarioStepRefType.REF, ScenarioStepRefType.PARTIAL_REF].includes(node.parent.refType)
) {
//
node.isQuoteScenarioStep = true; //
node.isRefScenarioStep = node.parent.refType === ScenarioStepRefType.REF; //
} else if (node.parent) {
//
node.isQuoteScenarioStep = node.parent.isQuoteScenarioStep; //
node.isRefScenarioStep = node.parent.isRefScenarioStep; //
}
return node;
});
}
scenarioTabs.value.push({
...defaultScenarioInfo,