refactor(缺陷管理): 详情增加缺陷历史展示

This commit is contained in:
RubyLiu 2024-02-21 19:04:33 +08:00 committed by 刘瑞斌
parent 4554dcec42
commit 52448dcb32
12 changed files with 567 additions and 59 deletions

View File

@ -211,3 +211,8 @@ export function batchAssociation(data: TableQueryParams) {
export function cancelAssociation(id: string) {
return MSR.get({ url: `${bugURL.getCancelDemandUrl}/${id}` });
}
// 缺陷管理-变更历史-列表
export function getChangeHistoryList(data: TableQueryParams) {
return MSR.post({ url: bugURL.getChangeHistoryListUrl, data });
}

View File

@ -64,3 +64,6 @@ export const getUnrelatedDemandListUrl = '/bug/case/un-relate/page';
export const getUnrelatedModuleTreeUrl = '/bug/case/un-relate/module/tree';
// 未关联的模块树 数量
export const getUnrelatedModuleTreeCountUrl = '/bug/case/un-relate/module/count';
// 缺陷管理-变更历史-列表
export const getChangeHistoryListUrl = '/bug/history/page';

View File

@ -0,0 +1,385 @@
<template>
<div class="flex flex-col">
<div>
<a-radio-group v-model:model-value="activeTab" type="button" size="small">
<a-radio v-for="item of responseRadios" :key="item.value" :value="item.value">
{{ t(item.label) }}
</a-radio>
</a-radio-group>
</div>
<div v-if="activeTab === 'jsonPath'" class="mt-[16px]">
<paramsTable
v-model:params="innerParams.jsonPath"
:selectable="false"
:columns="jsonPathColumns"
:scroll="{ minWidth: '700px' }"
:default-param-item="jsonPathDefaultParamItem"
@change="handleJsonPathChange"
@more-action-select="(e,r)=> handleExtractParamMoreActionSelect(e,r as ExpressionConfig)"
>
<template #expression="{ record }">
<a-popover
position="tl"
:disabled="!record.expression || record.expression.trim() === ''"
class="ms-params-input-popover"
>
<template #content>
<div class="param-popover-title">
{{ t('apiTestDebug.expression') }}
</div>
<div class="param-popover-value">
{{ record.expression }}
</div>
</template>
<a-input
v-model:model-value="record.expression"
class="ms-params-input"
:max-length="255"
@input="handleExpressionChange"
@change="handleExpressionChange"
>
<template #suffix>
<a-tooltip :disabled="!disabledExpressionSuffix">
<template #content>
<div>{{ t('apiTestDebug.expressionTip1') }}</div>
<div>{{ t('apiTestDebug.expressionTip2') }}</div>
<div>{{ t('apiTestDebug.expressionTip3') }}</div>
</template>
<MsIcon
type="icon-icon_flashlamp"
:size="15"
:class="
disabledExpressionSuffix ? 'ms-params-input-suffix-icon--disabled' : 'ms-params-input-suffix-icon'
"
@click.stop="() => showFastExtraction(record, RequestExtractExpressionEnum.JSON_PATH)"
/>
</a-tooltip>
</template>
</a-input>
</a-popover>
</template>
<template #operationPre="{ record }">
<a-popover
v-model:popupVisible="record.moreSettingPopoverVisible"
position="tl"
trigger="click"
:title="t('common.setting')"
:content-style="{ width: '480px' }"
>
<template #content>
<moreSetting v-model:config="activeRecord" is-popover class="mt-[12px]" />
<div class="flex items-center justify-end gap-[8px]">
<a-button type="secondary" size="mini" @click="record.moreSettingPopoverVisible = false">
{{ t('common.cancel') }}
</a-button>
<a-button type="primary" size="mini" @click="() => applyMoreSetting(record)">
{{ t('common.confirm') }}
</a-button>
</div>
</template>
<span class="invisible relative"></span>
</a-popover>
</template>
</paramsTable>
</div>
<div v-if="activeTab === 'xPath'" class="mt-[16px]">
<div class="text-[var(--color-text-1)]">{{ t('ms.assertion.responseContentType') }}</div>
<a-radio-group
v-model:model-value="innerParams.xPath.responseFormat"
class="mt-[16px]"
type="button"
size="small"
>
<a-radio value="XML">XML</a-radio>
<a-radio value="HTML">HTML</a-radio>
</a-radio-group>
<paramsTable
v-model:params="innerParams.xPath.data"
class="mt-[16px]"
:selectable="false"
:columns="xPathColumns"
:scroll="{ minWidth: '700px' }"
:default-param-item="xPathDefaultParamItem"
@change="handleXPathChange"
@more-action-select="(e,r)=> handleExtractParamMoreActionSelect(e,r as ExpressionConfig)"
>
<template #expression="{ record }">
<a-popover
position="tl"
:disabled="!record.expression || record.expression.trim() === ''"
class="ms-params-input-popover"
>
<template #content>
<div class="param-popover-title">
{{ t('apiTestDebug.expression') }}
</div>
<div class="param-popover-value">
{{ record.expression }}
</div>
</template>
<a-input
v-model:model-value="record.expression"
class="ms-params-input"
:max-length="255"
@input="handleExpressionChange"
@change="handleExpressionChange"
>
<template #suffix>
<a-tooltip :disabled="!disabledExpressionSuffix">
<template #content>
<div>{{ t('apiTestDebug.expressionTip1') }}</div>
<div>{{ t('apiTestDebug.expressionTip2') }}</div>
<div>{{ t('apiTestDebug.expressionTip3') }}</div>
</template>
<MsIcon
type="icon-icon_flashlamp"
:size="15"
:class="
disabledExpressionSuffix ? 'ms-params-input-suffix-icon--disabled' : 'ms-params-input-suffix-icon'
"
@click.stop="() => showFastExtraction(record, RequestExtractExpressionEnum.JSON_PATH)"
/>
</a-tooltip>
</template>
</a-input>
</a-popover>
</template>
<template #operationPre="{ record }">
<a-popover
v-model:popupVisible="record.moreSettingPopoverVisible"
position="tl"
trigger="click"
:title="t('common.setting')"
:content-style="{ width: '480px' }"
>
<template #content>
<moreSetting v-model:config="activeRecord" is-popover class="mt-[12px]" />
<div class="flex items-center justify-end gap-[8px]">
<a-button type="secondary" size="mini" @click="record.moreSettingPopoverVisible = false">
{{ t('common.cancel') }}
</a-button>
<a-button type="primary" size="mini" @click="() => applyMoreSetting(record)">
{{ t('common.confirm') }}
</a-button>
</div>
</template>
<span class="invisible relative"></span>
</a-popover>
</template>
</paramsTable>
</div>
</div>
<fastExtraction v-model:visible="fastExtractionVisible" :config="activeRecord" @apply="handleFastExtractionApply" />
</template>
<script setup lang="ts">
import { defineModel } from 'vue';
import { statusCodeOptions } from '@/components/pure/ms-advance-filter';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import fastExtraction from '@/views/api-test/components/fastExtraction/index.vue';
import moreSetting from '@/views/api-test/components/fastExtraction/moreSetting.vue';
import paramsTable, { type ParamTableColumn } from '@/views/api-test/components/paramTable.vue';
import { useI18n } from '@/hooks/useI18n';
import {
ExecuteConditionProcessor,
ExpressionType,
JSONPathExtract,
RegexExtract,
XPathExtract,
} from '@/models/apiTest/debug';
import {
RequestExtractEnvType,
RequestExtractExpressionEnum,
RequestExtractExpressionRuleType,
RequestExtractResultMatchingRule,
RequestExtractScope,
ResponseBodyXPathAssertionFormat,
} from '@/enums/apiEnum';
interface Param {
[key: string]: any;
}
const emit = defineEmits<{
(e: 'update:data', data: ExecuteConditionProcessor): void;
(e: 'copy'): void;
(e: 'delete', id: number): void;
(e: 'change'): void;
}>();
const innerParams = defineModel<Param>('modelValue', {
default: { jsonPath: [], xPath: { responseFormat: 'XML', data: [] } },
});
const activeTab = ref('jsonPath');
const extractParamsTableRef = ref<InstanceType<typeof paramsTable>>();
const fastExtractionVisible = ref(false);
const disabledExpressionSuffix = ref(false);
export type ExpressionConfig = (RegexExtract | JSONPathExtract | XPathExtract) & Record<string, any>;
const defaultExtractParamItem: ExpressionConfig = {
enable: true,
variableName: '',
variableType: RequestExtractEnvType.TEMPORARY,
extractScope: RequestExtractScope.BODY,
expression: '',
extractType: RequestExtractExpressionEnum.REGEX,
expressionMatchingRule: RequestExtractExpressionRuleType.EXPRESSION,
resultMatchingRule: RequestExtractResultMatchingRule.RANDOM,
resultMatchingRuleNum: 1,
responseFormat: ResponseBodyXPathAssertionFormat.XML,
moreSettingPopoverVisible: false,
};
const activeRecord = ref({ ...defaultExtractParamItem }); //
const responseRadios = [
{ label: 'ms.assertion.jsonPath', value: 'jsonPath' },
{ label: 'ms.assertion.xpath', value: 'xPath' },
{ label: 'ms.assertion.document', value: 'document' },
{ label: 'ms.assertion.regular', value: 'regular' },
{ label: 'ms.assertion.script', value: 'script' },
];
const jsonPathColumns: ParamTableColumn[] = [
{
title: 'ms.assertion.expression',
dataIndex: 'expression',
slotName: 'expression',
},
{
title: 'ms.assertion.matchCondition',
dataIndex: 'matchCondition',
slotName: 'matchCondition',
options: statusCodeOptions,
},
{
title: 'ms.assertion.matchValue',
dataIndex: 'matchValue',
slotName: 'matchValue',
},
{
title: '',
slotName: 'operation',
fixed: 'right',
width: 130,
hasDisable: true,
moreAction: [
{
eventTag: 'copy',
label: 'common.copy',
},
{
eventTag: 'setting',
label: 'common.setting',
},
],
},
];
const jsonPathDefaultParamItem = {
expression: '',
matchCondition: '',
matchValue: '',
enable: true,
};
const handleJsonPathChange = () => {
console.log('jsonPath change');
};
function handleExpressionChange(val: string) {
extractParamsTableRef.value?.addTableLine(val, 'expression');
}
const xPathColumns: ParamTableColumn[] = [
{
title: 'ms.assertion.expression',
dataIndex: 'expression',
slotName: 'expression',
},
{
title: '',
slotName: 'operation',
fixed: 'right',
width: 130,
hasDisable: true,
moreAction: [
{
eventTag: 'copy',
label: 'common.copy',
},
{
eventTag: 'setting',
label: 'common.setting',
},
],
},
];
const xPathDefaultParamItem = {
expression: '',
matchCondition: '',
matchValue: '',
enable: true,
};
const handleXPathChange = () => {
console.log('jsonPath change');
};
/**
* 提取参数表格-应用更多设置
*/
function applyMoreSetting(record: ExpressionConfig) {
// condition.value.extractParams = condition.value.extractParams?.map((e) => {
// if (e.id === activeRecord.value.id) {
// record.moreSettingPopoverVisible = false;
// return {
// ...activeRecord.value,
// moreSettingPopoverVisible: false,
// } as any; // TOOD:
// }
// return e;
// });
// emit('change');
}
/**
* 提取参数表格-保存快速提取的配置
*/
function handleFastExtractionApply(config: RegexExtract | JSONPathExtract | XPathExtract) {
// condition.value.extractParams = condition.value.extractParams?.map((e) => {
// if (e.id === activeRecord.value.id) {
// return {
// ...e,
// ...config,
// };
// }
// return e;
// });
// fastExtractionVisible.value = false;
// nextTick(() => {
// extractParamsTableRef.value?.addTableLine();
// });
// emit('change');
}
/**
* 处理提取参数表格更多操作
*/
function handleExtractParamMoreActionSelect(event: ActionsItem, record: ExpressionConfig) {
activeRecord.value = { ...record };
if (event.eventTag === 'copy') {
emit('copy');
} else if (event.eventTag === 'setting') {
record.moreSettingPopoverVisible = true;
}
}
function showFastExtraction(record: ExpressionConfig, type: ExpressionType) {
activeRecord.value = { ...record, extractType: type };
fastExtractionVisible.value = true;
}
const { t } = useI18n();
</script>

View File

@ -1,18 +1,13 @@
<template>
<div>
<div>
<a-radio-group v-model:model-value="innerParams.type" type="button" size="small">
<a-radio v-for="item of responseRadios" :key="item.value" :value="item.value">
{{ t(item.label) }}
</a-radio>
</a-radio-group>
</div>
<div v-if="innerParams.type === 'jsonPath'">
<MsJsonPathPicker data="" />
</div>
<div v-else-if="innerParams.type === 'xPath'">
<MsXPathPicker :xml-string="innerParams.response" />
</div>
<paramsTable
v-model:params="innerParams"
:selectable="false"
:columns="columns"
:scroll="{ minWidth: '700px' }"
:default-param-item="defaultParamItem"
@change="handleParamTableChange"
/>
</div>
</template>
@ -20,28 +15,77 @@
import { defineModel } from 'vue';
import { statusCodeOptions } from '@/components/pure/ms-advance-filter/index';
import MsJsonPathPicker from '@/components/pure/ms-jsonpath-picker/index.vue';
import MsXPathPicker from '@/components/pure/ms-jsonpath-picker/xpath.vue';
import paramsTable, { type ParamTableColumn } from '@/views/api-test/components/paramTable.vue';
import { useI18n } from '@/hooks/useI18n';
interface Param {
[key: string]: any;
}
const innerParams = defineModel<Param>('modelValue', { default: { type: 'jsonPath' } });
const innerParams = defineModel<Param[]>('modelValue', { default: [] });
const emit = defineEmits<{
(e: 'change'): void; //
}>();
const { t } = useI18n();
const responseRadios = [
{ label: 'ms.assertion.jsonPath', value: 'jsonPath' },
{ label: 'ms.assertion.xpath', value: 'xPath' },
{ label: 'ms.assertion.document', value: 'document' },
{ label: 'ms.assertion.regular', value: 'regular' },
{ label: 'ms.assertion.script', value: 'script' },
const defaultParamItem = {
responseHeader: '',
matchCondition: '',
matchValue: '',
enable: true,
};
const responseHeaderOption = [
{ label: 'Accept', value: 'accept' },
{ label: 'Accept-Encoding', value: 'acceptEncoding' },
{ label: 'Accept-Language', value: 'acceptLanguage' },
{ label: 'Cache-Control', value: 'cacheControl' },
{ label: 'Content-Type', value: 'contentType' },
{ label: 'Content-Length', value: 'contentLength' },
{ label: 'User-Agent', value: 'userAgent' },
{ label: 'Referer', value: 'referer' },
{ label: 'Cookie', value: 'cookie' },
{ label: 'Authorization', value: 'authorization' },
{ label: 'If-None-Match', value: 'ifNoneMatch' },
{ label: 'If-Modified-Since', value: 'ifModifiedSince' },
];
const columns: ParamTableColumn[] = [
{
title: 'ms.assertion.responseHeader', //
dataIndex: 'responseHeader',
slotName: 'responseHeader',
showInTable: true,
showDrag: true,
options: responseHeaderOption,
},
{
title: 'ms.assertion.matchCondition', //
dataIndex: 'matchCondition',
slotName: 'matchCondition',
showInTable: true,
showDrag: true,
options: statusCodeOptions,
},
{
title: 'ms.assertion.matchValue', //
dataIndex: 'matchValue',
slotName: 'matchValue',
showInTable: true,
showDrag: true,
},
{
title: '',
columnTitle: 'common.operation',
slotName: 'operation',
width: 50,
showInTable: true,
showDrag: true,
},
];
function handleParamTableChange(resultArr: any[], isInit?: boolean) {
innerParams.value = [...resultArr];
if (!isInit) {
emit('change');
}
}
</script>

View File

@ -61,6 +61,7 @@
v-model:statusCode="codeTabState.statusCode"
/>
<ResponseHeaderTab v-if="valueKey === 'responseHeader'" />
<ResponseBodyTab v-if="valueKey === 'responseBody'" />
<ResponseTimeTab v-if="valueKey === 'responseTime'" />
<VariableTab v-if="valueKey === 'variable'" />
<ScriptTab v-if="valueKey === 'script'" />
@ -76,6 +77,7 @@
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import ResponseBodyTab from './comp/ResponseBodyTab.vue';
import ResponseHeaderTab from './comp/ResponseHeaderTab.vue';
import ResponseTimeTab from './comp/ResponseTimeTab.vue';
import ScriptTab from './comp/ScriptTab.vue';

View File

@ -14,4 +14,6 @@ export default {
'ms.assertion.document': '文档',
'ms.assertion.regular': '正则',
'ms.assertion.script': '脚本',
'ms.assertion.expression': '表达式',
'ms.assertion.responseContentType': '响应内容类型',
};

View File

@ -1,8 +1,8 @@
export { default as FilterForm } from './FilterForm.vue';
export { default as MsAdvanceFilter } from './index.vue';
const IN = { label: 'advanceFilter.operator.in', value: 'in' };
const NOT_IN = { label: 'advanceFilter.operator.not_in', value: 'not_in' };
// const IN = { label: 'advanceFilter.operator.in', value: 'in' };
// const NOT_IN = { label: 'advanceFilter.operator.not_in', value: 'not_in' };
const LIKE = { label: 'advanceFilter.operator.like', value: 'like' };
const NOT_LIKE = { label: 'advanceFilter.operator.not_like', value: 'not_like' };
const GT = { label: 'advanceFilter.operator.gt', value: 'gt' };
@ -14,18 +14,16 @@ const NOT_EQUAL = { label: 'advanceFilter.operator.notEqual', value: 'not_equal'
const BETWEEN = { label: 'advanceFilter.operator.between', value: 'between' };
export const OPERATOR_MAP = {
string: [LIKE, NOT_LIKE, IN, NOT_IN, EQUAL, NOT_EQUAL],
string: [LIKE, NOT_LIKE, EQUAL, NOT_EQUAL],
number: [GT, GE, LT, LE, EQUAL, NOT_EQUAL, BETWEEN],
date: [GT, GE, LT, LE, EQUAL, NOT_EQUAL, BETWEEN],
array: [IN, NOT_IN],
array: [BETWEEN],
};
export const timeSelectOptions = [GE, LE];
export const statusCodeOptions = [
{ label: 'ms.assertion.noValidation', value: 'none' },
IN,
NOT_IN,
EQUAL,
NOT_EQUAL,
GT,
@ -133,7 +131,7 @@ export const CustomTypeMaps: Record<string, any> = {
},
};
export const MULTIPLE_OPERATOR_LIST = ['in', 'not_in', 'between'];
export const MULTIPLE_OPERATOR_LIST = ['between'];
export function isMutipleOperator(operator: string) {
return MULTIPLE_OPERATOR_LIST.includes(operator);

View File

@ -12,7 +12,7 @@
@search="emit('keywordSearch', innerKeyword, filterResult)"
@clear="handleClear"
></a-input-search>
<MsTag
<!-- <MsTag
:type="visible ? 'primary' : 'default'"
:theme="visible ? 'lightOutLine' : 'outline'"
size="large"
@ -27,7 +27,7 @@
{{ t('common.filter') }}
</span>
</span>
</MsTag>
</MsTag> -->
<MsTag no-margin size="large" class="cursor-pointer" theme="outline" @click="handleRefresh">
<MsIcon class="text-[var(color-text-4)]" :size="16" type="icon-icon_reset_outlined" />
</MsTag>

View File

@ -206,6 +206,14 @@
<a-checkbox v-model:model-value="record[columnConfig.dataIndex as string]" @change="(val) => addTableLine(val)" />
</template>
<template #operation="{ record, rowIndex, columnConfig }">
<a-switch
v-if="columnConfig.hasDisable"
v-model:model-value="record.disable"
size="small"
type="line"
class="mr-[8px]"
@change="(val) => addTableLine(val, 'disable')"
/>
<slot name="operationPre" :record="record" :row-index="rowIndex" :column-config="columnConfig"></slot>
<MsTableMoreAction
v-if="columnConfig.moreAction"
@ -226,19 +234,14 @@
</div>
</template>
</a-trigger>
<a-switch
v-if="columnConfig.hasDisable"
v-model:model-value="record.disable"
size="small"
type="line"
@change="(val) => addTableLine(val, 'disable')"
/>
<div>
<icon-minus-circle
v-if="paramsLength > 1 && rowIndex !== paramsLength - 1"
class="cursor-pointer text-[var(--color-text-4)]"
class="ml-[8px] cursor-pointer text-[var(--color-text-4)]"
size="20"
@click="deleteParam(rowIndex)"
/>
</div>
</template>
<template #responseHeader="{ record, columnConfig }">
<a-select v-model="record.responseHeader" class="param-input" @change="(val) => addTableLine(val as string)">

View File

@ -100,6 +100,12 @@
</template>
<CommentTab ref="commentRef" :bug-id="detailInfo.id" />
</a-tab-pane>
<a-tab-pane key="history">
<template #title>
{{ t('bugManagement.detail.changeHistory') }}
</template>
<BugHistoryTab :bug-id="detailInfo.id" />
</a-tab-pane>
</a-tabs>
</div>
</div>
@ -166,6 +172,7 @@
import MsDetailDrawer from '@/components/business/ms-detail-drawer/index.vue';
import BugCaseTab from './bugCaseTab.vue';
import BugDetailTab from './bugDetailTab.vue';
import BugHistoryTab from './bugHistoryTab.vue';
import CommentTab from './commentTab.vue';
import {

View File

@ -0,0 +1,64 @@
<template>
<ms-base-table class="mt-[16px]" v-bind="propsRes" v-on="propsEvent">
<template #changeNumber="{ record }">
<span>{{ record.id }}</span>
<!-- TODO: 先不上 -->
<!-- <a-tag size="small" class="ml-[4px]">{{ t('bugManagement.history.current') }}</a-tag> -->
</template>
</ms-base-table>
</template>
<script lang="ts" setup>
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import { getChangeHistoryList } from '@/api/modules/bug-management';
import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store';
const { t } = useI18n();
const appStore = useAppStore();
const props = defineProps<{
bugId: string;
}>();
const columns: MsTableColumn = [
{
title: 'bugManagement.history.changeNumber',
slotName: 'changeNumber',
dataIndex: 'id',
width: 200,
},
{
title: 'bugManagement.history.operationMan',
dataIndex: 'createUserName',
showTooltip: true,
width: 200,
},
{
title: 'bugManagement.history.updateTime',
dataIndex: 'createTime',
width: 200,
},
];
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(getChangeHistoryList, {
heightUsed: 240,
columns,
scroll: { x: '100%' },
selectable: false,
noDisable: false,
pageSimple: true,
debug: true,
});
const fetchData = async (id: string) => {
setLoadListParams({ sourceId: id, projectId: appStore.currentProjectId });
await loadList();
};
watchEffect(() => {
fetchData(props.bugId);
});
</script>

View File

@ -67,6 +67,7 @@ export default {
tag: '标签',
detail: '详情',
case: '用例',
changeHistory: '变更历史',
comment: '评论',
shareTip: '分享链接已复制到剪贴板',
deleteTitle: '确认删除 {name} 吗?',
@ -106,18 +107,12 @@ export default {
deleteTime: '删除时间',
deleteMan: '删除人',
},
severityO: {
fatal: '致命',
serious: '严重',
general: '一般',
reminder: '提醒',
},
statusO: {
create: '新建',
processing: '处理中',
resolved: '已解决',
closed: '已关闭',
refused: '已拒绝',
history: {
changeNumber: '变更序号',
operationMan: '操作人',
updateTime: '更新时间',
restore: '恢复',
current: '当前',
},
},
};