feat(缺陷管理): 查看详情&批量编辑&高级搜索

This commit is contained in:
RubyLiu 2024-02-05 18:49:00 +08:00 committed by Craftsman
parent a39739e373
commit ca1955a99e
7 changed files with 120 additions and 216 deletions

View File

@ -70,7 +70,7 @@
"resize-observer-polyfill": "^1.5.1",
"sortablejs": "^1.15.0",
"tippy.js": "^6.3.7",
"vue": "^3.3.4",
"vue": "^3.4.15",
"vue-dompurify-html": "^4.1.4",
"vue-draggable-plus": "^0.3.5",
"vue-echarts": "^6.6.1",
@ -140,7 +140,7 @@
"stylelint-order": "^5.0.0",
"tailwindcss": "^3.3.3",
"typescript": "^4.9.5",
"unplugin-auto-import": "^0.16.6",
"unplugin-auto-import": "^0.16.7",
"unplugin-vue-components": "^0.24.1",
"vite": "^3.2.7",
"vite-plugin-compression": "^0.5.1",
@ -161,4 +161,4 @@
"rollup": "^2.79.1",
"gifsicle": "5.2.0"
}
}
}

View File

@ -80,7 +80,7 @@
const handleResetFilter = () => {
filterResult.value = { ...defaultFilterResult };
emit('keywordSearch', '', { ...defaultFilterResult });
emit('advSearch', { ...defaultFilterResult });
};
const handleFilter = (filter: FilterResult) => {

View File

@ -46,7 +46,7 @@ export interface BugEditCustomFieldItem {
type: string;
value: string;
}
export type BugBatchUpdateFiledType = 'single_select' | 'multiple_select' | 'tag' | 'input' | 'user_selector' | 'date';
export type BugBatchUpdateFiledType = 'single_select' | 'multiple_select' | 'tags' | 'input' | 'user_selector' | 'date';
export interface BugBatchUpdateFiledForm {
attribute: string;
value: string[];

View File

@ -24,19 +24,16 @@
>
<a-select v-model:model-value="form.attribute" @change="handleArrtibuteChange">
<a-optgroup :label="t('bugManagement.batchUpdate.systemFiled')">
<a-option
v-for="item in systemOptionList"
:key="item.value"
:disabled="form.attribute === 'status'"
:value="item.value"
>{{ item.label }}</a-option
>
</a-optgroup>
<a-optgroup :label="t('bugManagement.batchUpdate.customFiled')">
<a-option v-for="item in customOptionList" :key="item.value" :value="item.value">{{
<a-option v-for="item in systemOptionList" :key="item.value" :value="item.value">{{
item.label
}}</a-option>
</a-optgroup>
<!-- V3.0 先不上自定义字段的批量更新 -->
<!-- <a-optgroup :label="t('bugManagement.batchUpdate.customFiled')">
<a-option v-for="item in customOptionList" :disabled="form.attribute === 'status'" :key="item.value" :value="item.value">{{
item.label
}}</a-option>
</a-optgroup> -->
</a-select>
</a-form-item>
<a-form-item
@ -67,7 +64,7 @@
:label="t('bugManagement.batchUpdate.update')"
:rules="[{ required: true }]"
>
<template v-if="valueMode === 'tag'">
<template v-if="valueMode === 'tags'">
<MsTagsInput v-model:modelValue="form.value" :disabled="!form.attribute"></MsTagsInput>
</template>
<template v-else-if="valueMode === 'user_selector'">
@ -131,6 +128,7 @@
import { updateBatchBug } from '@/api/modules/bug-management';
import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store';
import { tableParamsToRequestParams } from '@/utils';
import type { BugBatchUpdateFiledType } from '@/models/bug-management';
import { BugBatchUpdateFiledForm, BugEditCustomField } from '@/models/bug-management';
@ -149,21 +147,17 @@
const appStore = useAppStore();
const selectCount = computed(() => props.selectParam.currentSelectCount);
const systemOptionList = computed(() => [
{
label: t('bugManagement.batchUpdate.handleUser'),
value: 'handleUser',
},
{
label: t('bugManagement.batchUpdate.tag'),
value: 'tag',
value: 'tags',
},
]);
const customOptionList = computed(() => {
return props.customFields.map((item) => ({
label: item.fieldName,
value: item.fieldId,
}));
});
// const customOptionList = computed(() => {
// return props.customFields.map((item) => ({
// label: item.fieldName,
// value: item.fieldId,
// }));
// });
const currentVisible = computed({
get() {
return props.visible;
@ -189,7 +183,15 @@
const formRef = ref<FormInstance>();
const formReset = () => {
form.attribute = '';
form.value = [];
form.inputValue = '';
form.append = false;
};
const handleCancel = () => {
formReset();
currentVisible.value = false;
loading.value = false;
};
@ -198,8 +200,8 @@
const handleArrtibuteChange = (value: SelectValue) => {
form.value = [];
form.inputValue = '';
if (value === 'tag') {
valueMode.value = 'tag';
if (value === 'tags') {
valueMode.value = 'tags';
showAppend.value = true;
} else if (value === 'handleUser') {
valueMode.value = 'user_selector';
@ -228,17 +230,13 @@
if (!errors) {
try {
loading.value = true;
const params = {
excludeIds: props.selectParam.excludeIds,
selectIds: props.selectParam.selectedIds,
selectAll: props.selectParam.selectAll,
//
condition: props.selectParam.condition,
const tmpObj = {
...tableParamsToRequestParams(props.selectParam),
projectId: appStore.currentProjectId,
[form.attribute]: form.value || form.inputValue,
append: form.append,
};
await updateBatchBug(params);
await updateBatchBug(tmpObj);
Message.success(t('common.deleteSuccess'));
handleCancel();
emit('submit');

View File

@ -88,20 +88,20 @@
<template #title>
{{ t('bugManagement.detail.detail') }}
</template>
<BugDetailTab :detail-info="detailInfo" />
<BugDetailTab />
</a-tab-pane>
<a-tab-pane key="case">
<template #title>
{{ t('bugManagement.detail.case') }}
<a-badge class="relative top-1 ml-1" :count="1000" :max-count="99" />
</template>
<BugCaseTab :detail-info="detailInfo" />
<BugCaseTab />
</a-tab-pane>
<a-tab-pane key="comment">
<template #title>
{{ t('bugManagement.detail.comment') }}
</template>
<CommentTab ref="commentRef" bug-id="1070838426116099" />
<CommentTab ref="commentRef" bug-id="detailInfo.id" />
</a-tab-pane>
</a-tabs>
</div>
@ -123,16 +123,17 @@
<!-- 自定义字段结束 -->
<div class="baseItem">
<span class="label"> {{ t('bugManagement.detail.tag') }}</span>
<!-- <MsTagsInput></MsTagsInput> -->
<MsTagsInput v-model:model-value="tags"></MsTagsInput>
</div>
<div class="baseItem">
<!-- 创建人 创建时间需求说先去掉 -->
<!-- <div class="baseItem">
<span class="label"> {{ t('bugManagement.detail.creator') }}</span>
<span>{{ detailInfo?.createUser }}</span>
<span>{{ detailInfo?.createUserName }}</span>
</div>
<div class="baseItem">
<span class="label"> {{ t('bugManagement.detail.createTime') }}</span>
<span>{{ dayjs(detailInfo?.createTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</div>
</div> -->
</div>
</template>
</MsSplitBox>
@ -153,7 +154,6 @@
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { Message } from '@arco-design/web-vue';
import dayjs from 'dayjs';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsFormCreate from '@/components/pure/ms-form-create/ms-form-create.vue';
@ -169,24 +169,29 @@
import BugDetailTab from './bugDetailTab.vue';
import CommentTab from './commentTab.vue';
import { createOrUpdateComment, deleteSingleBug, followBug, getBugDetail } from '@/api/modules/bug-management/index';
import {
createOrUpdateComment,
deleteSingleBug,
followBug,
getBugDetail,
getTemplateById,
} from '@/api/modules/bug-management/index';
import useFullScreen from '@/hooks/useFullScreen';
import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal';
import { useAppStore } from '@/store';
import useFeatureCaseStore from '@/store/modules/case/featureCase';
import { characterLimit } from '@/utils';
import type { CaseManagementTable, CustomAttributes, TabItemType } from '@/models/caseManagement/featureCase';
import { BugEditCustomField, BugEditFormObject } from '@/models/bug-management';
import { SelectValue } from '@/models/projectManagement/menuManagement';
import { RouteEnum } from '@/enums/routeEnum';
const router = useRouter();
const detailDrawerRef = ref<InstanceType<typeof MsDetailDrawer>>();
const wrapperRef = ref();
const { isFullScreen, toggleFullScreen } = useFullScreen(wrapperRef);
const featureCaseStore = useFeatureCaseStore();
const { t } = useI18n();
const { openModal } = useModal();
const { openDeleteModal } = useModal();
const props = defineProps<{
visible: boolean;
@ -197,39 +202,68 @@
pageChange: (page: number) => Promise<void>; //
}>();
const emit = defineEmits(['update:visible']);
const appStore = useAppStore();
const commentContent = ref('');
const commentRef = ref();
const noticeUserIds = ref<string[]>([]); // ids
const fApi = ref();
const formRules = ref<FormItem[]>([]); //
const formItem = ref<FormRuleItem[]>([]); //
const currentProjectId = computed(() => appStore.currentProjectId);
const showDrawerVisible = defineModel<boolean>('visible', { default: false });
const showDrawerVisible = ref<boolean>(false);
const tabSettingList = computed(() => {
return featureCaseStore.tabSettingList;
});
const tabSetting = ref<TabItemType[]>([...tabSettingList.value]);
const activeTab = ref<string | number>('detail');
const detailInfo = ref<Record<string, any>>({
tags: [],
id: '',
createUser: '',
createTime: '',
description: '',
followFlag: false,
templateId: '',
title: '',
});
const customFields = ref<CustomAttributes[]>([]);
const detailInfo = ref<Record<string, any>>({}); // loadBug
const tags = ref([]);
function loadedBug(detail: CaseManagementTable) {
//
const getFormRules = (arr: BugEditCustomField[]) => {
formRules.value = [];
if (Array.isArray(arr) && arr.length) {
formRules.value = arr.map((item) => {
return {
type: item.type,
name: item.fieldId,
label: item.fieldName,
value: item.value,
options: item.platformOptionJson ? JSON.parse(item.platformOptionJson) : item.options,
required: item.required as boolean,
props: {
modelValue: item.value,
options: item.platformOptionJson ? JSON.parse(item.platformOptionJson) : item.options,
},
};
});
}
};
const templateChange = async (v: SelectValue) => {
if (v) {
try {
const res = await getTemplateById({ projectId: appStore.currentProjectId, id: v });
getFormRules(res.customFields);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
};
async function loadedBug(detail: BugEditFormObject) {
detailInfo.value = { ...detail };
customFields.value = detailInfo.value.customFields;
const { templateId } = detail;
// tag
tags.value = detail.tags || [];
//
await templateChange(templateId);
const tmpObj = {};
if (detail.customFields && Array.isArray(detail.customFields)) {
detail.customFields.forEach((item) => {
tmpObj[item.id] = item.value;
});
}
//
fApi.value.setValue(tmpObj);
}
const editLoading = ref<boolean>(false);
@ -276,15 +310,9 @@
//
function deleteHandler() {
const { id, name } = detailInfo.value;
openModal({
type: 'error',
title: t('caseManagement.featureCase.deleteCaseTitle', { name: characterLimit(name) }),
content: t('caseManagement.featureCase.beforeDeleteCase'),
okText: t('common.confirmDelete'),
cancelText: t('common.cancel'),
okButtonProps: {
status: 'danger',
},
openDeleteModal({
title: t('bugManagement.detail.deleteTitle', { name: characterLimit(name) }),
content: t('bugManagement.detail.deleteContent'),
onBeforeOk: async () => {
try {
const params = {
@ -301,15 +329,9 @@
console.log(error);
}
},
hideCancel: false,
});
}
const formRules = ref<FormItem[]>([]);
const formItem = ref<FormRuleItem[]>([]);
const isDisabled = ref<boolean>(false);
//
const options = {
resetBtn: false, //
@ -336,26 +358,6 @@
},
};
const fApi = ref(null);
//
function initForm() {
formRules.value = customFields.value.map((item: any) => {
return {
type: item.type,
name: item.fieldId,
label: item.fieldName,
value: JSON.parse(item.defaultValue),
required: item.required,
options: item.options || [],
props: {
modelValue: JSON.parse(item.defaultValue),
disabled: isDisabled.value,
options: item.options || [],
},
};
}) as FormItem[];
}
async function publishHandler(currentContent: string) {
const regex = /data-id="([^"]*)"/g;
const matchesNotifier = currentContent.match(regex);
@ -365,7 +367,6 @@
}
try {
const params = {
// TODO
bugId: detailInfo.value.id,
notifier: notifiers,
replyUser: '',
@ -381,36 +382,6 @@
console.log(error);
}
}
watch(
() => customFields.value,
() => {
initForm();
},
{ deep: true }
);
watch(
() => props.visible,
(val) => {
showDrawerVisible.value = val;
}
);
watch(
() => showDrawerVisible.value,
(val) => {
emit('update:visible', val);
}
);
watch(
() => tabSettingList.value,
() => {
tabSetting.value = featureCaseStore.getTab();
},
{ deep: true, immediate: true }
);
</script>
<style scoped lang="less">

View File

@ -33,19 +33,6 @@
<template #num="{ record, rowIndex }">
<a-button type="text" class="px-0" @click="handleShowDetail(record.id, rowIndex)">{{ record.num }}</a-button>
</template>
<!-- 严重程度 -->
<template #severity="{ record }">
<MsEditComp mode="select" :options="severityOption" :default-value="record.severity" />
</template>
<!-- 状态 -->
<template #status="{ record }">
<MsEditComp mode="select" :options="statusOption" :default-value="record.statusName" />
</template>
<template #numberOfCase="{ record }">
<span class="cursor-pointer text-[rgb(var(--primary-5))]" @click="jumpToTestPlan(record)">{{
record.memberCount
}}</span>
</template>
<template #operation="{ record }">
<div class="flex flex-row flex-nowrap">
<span v-permission="['PROJECT_BUG:READ+ADD']" class="flex flex-row">
@ -150,7 +137,6 @@
import useTable from '@/components/pure/ms-table/useTable';
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import MsEditComp from '@/components/business/ms-edit-comp';
import BatchEditModal from './components/batchEditModal.vue';
import BugDetailDrawer from './components/bug-detail-drawer.vue';
import DeleteModal from './components/deleteModal.vue';
@ -164,13 +150,12 @@
getExportConfig,
syncBugOpenSource,
} from '@/api/modules/bug-management';
import { updateOrAddProjectUserGroup } from '@/api/modules/project-management/usergroup';
import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal';
import router from '@/router';
import { useAppStore, useTableStore } from '@/store';
import useLicenseStore from '@/store/modules/setting/license';
import { customFieldToColumns } from '@/utils';
import { customFieldToColumns, tableParamsToRequestParams } from '@/utils';
import { BugEditCustomField, BugListItem } from '@/models/bug-management';
import { RouteEnum } from '@/enums/routeEnum';
@ -230,29 +215,8 @@
{
title: 'bugManagement.bugName',
dataIndex: 'title',
type: FilterType.SELECT,
selectProps: {
mode: 'static',
options: [
{
label: 'title',
value: 'title',
},
{
label: 'name',
value: 'name',
},
],
},
},
{
title: 'bugManagement.severity',
dataIndex: 'severity',
type: FilterType.SELECT,
selectProps: {
mode: 'static',
multiple: true,
},
type: FilterType.INPUT,
backendType: BackEndEnum.STRING,
},
{
title: 'bugManagement.createTime',
@ -280,16 +244,9 @@
{
title: 'bugManagement.bugName',
dataIndex: 'title',
width: 300,
width: 200,
showTooltip: true,
},
{
title: 'bugManagement.severity',
slotName: 'severity',
width: 102,
showDrag: true,
dataIndex: 'severity',
},
{
title: 'bugManagement.status',
dataIndex: 'statusName',
@ -361,16 +318,6 @@
const customColumns = await getCustomFieldColumns();
await tableStore.initColumn(TableKeyEnum.BUG_MANAGEMENT, columns.concat(customColumns), 'drawer');
const handleNameChange = async (record: BugListItem) => {
try {
await updateOrAddProjectUserGroup(record);
Message.success(t('common.updateSuccess'));
return true;
} catch (error) {
return false;
}
};
const { propsRes, propsEvent, setKeyword, setAdvanceFilter, setLoadListParams, setProps, resetSelector, loadList } =
useTable(
getBugList,
@ -382,8 +329,7 @@
showSetting: true,
scroll: { x: '1800px' },
},
undefined,
(record) => handleNameChange(record)
(record: TableData) => ({ ...record, handleUser: record.handleUserName })
);
const tableBatchActions = {
@ -503,30 +449,16 @@
};
const handleBatchDelete = (params: BatchActionQueryParams) => {
// TODO: id
const internalCount = 2;
const externalCount = 0;
openDeleteModal({
title: t('bugManagement.deleteCount', { count: params.selectedIds?.length }),
content: `${internalCount ? t('bugManagement.deleteTipInternal', { count: internalCount }) : ''}${
internalCount ? ';' : ''
} ${
externalCount
? t('bugManagement.deleteTipExternal', {
count: externalCount,
})
: ''
}`,
content: t('bugManagement.deleteTip'),
onBeforeOk: async () => {
try {
const { selectedIds, selectAll, excludeIds } = params;
await deleteBatchBug({
selectIds: selectedIds || [],
selectAll,
excludeIds,
condition: { keyword: keyword.value },
const tmpObj = {
...tableParamsToRequestParams(params),
projectId: projectId.value,
});
};
await deleteBatchBug(tmpObj);
Message.success(t('common.deleteSuccess'));
fetchData();
} catch (error) {

View File

@ -32,6 +32,7 @@ export default {
deleteCount: '确认删除 {count} 个缺陷吗?',
deleteTipInternal: '删除后,{count} 条 MeterSphere 创建的缺陷进入回收站',
deleteTipExternal: '{count} 条第三方平台同步的缺陷将不做回收',
deleteTip: 'Metersphere进入回收站, 三方平台的不做回收',
edit: {
defaultSystemTemplate: '默认为系统模板',
content: '缺陷内容',
@ -67,6 +68,8 @@ export default {
detail: '详情',
case: '用例',
comment: '评论',
deleteTitle: '确认删除 {name} 吗?',
deleteContent: '删除后MeterSphere 创建的缺陷进入回收站;第三方平台同步的缺陷将不做回收',
},
batchUpdate: {
attribute: '选择属性',