feat(缺陷管理): 支持Jira默认模板

--bug=1036246 --user=宋昌昌 【缺陷管理】使用JIRA默认模板创建缺陷失败 https://www.tapd.cn/55049933/s/1473618
This commit is contained in:
song-cc-rock 2024-03-12 21:00:36 +08:00 committed by 刘瑞斌
parent effb35b4e1
commit c5602b2033
26 changed files with 341 additions and 121 deletions

View File

@ -13,4 +13,6 @@ public class PlatformCustomFieldItemDTO extends PlatformCustomFieldDTO {
private String defaultValue;
private Boolean supportSearch;
private String searchMethod;
private String placeHolder;
private Boolean systemField;
}

View File

@ -8,70 +8,69 @@ public enum PlatformCustomFieldType {
/**
* 输入框
*/
INPUT(false, "input"),
INPUT(false),
/**
* 文本框
*/
TEXTAREA(false, "textarea"),
TEXTAREA(false),
/**
* 单选下拉框框
*/
SELECT(true, "select"),
SELECT(true),
/**
* 多选下拉框框
*/
MULTIPLE_SELECT(true, "multipleSelect"),
MULTIPLE_SELECT(true),
/**
* 单选框
*/
RADIO(true, "radio"),
RADIO(true),
/**
* 复选框
*/
CHECKBOX(true, "checkbox"),
CHECKBOX(true),
/**
* 单选成员
*/
MEMBER(true, "member"),
MEMBER(true),
/**
* 多选成员
*/
MULTIPLE_MEMBER(true, "multipleMember"),
MULTIPLE_MEMBER(true),
/**
* 日期
*/
DATE(false, "date"),
DATE(false),
/**
* 日期时间
*/
DATETIME(false, "datetime"),
DATETIME(false),
/**
* 整型
*/
INT(false, "int"),
INT(false),
/**
* 浮点型
*/
FLOAT(false, "float"),
FLOAT(false),
/**
* 多值输入框标签输入框
*/
MULTIPLE_INPUT(false, "multipleInput"),
MULTIPLE_INPUT(false),
// (第三方平台单独的自定义类型)
/**
* 级联选择
*/
CASCADE_SELECT(true, "cascadingSelect"),
CASCADER(true),
/**
* 富文本
*/
RICH_TEXT(false, "richText");
RICH_TEXT(false);
private final Boolean hasOption;
private final String type;
PlatformCustomFieldType(Boolean hasOption, String type) {
PlatformCustomFieldType(Boolean hasOption) {
this.hasOption = hasOption;
this.type = type;
}
}

View File

@ -8,73 +8,65 @@ public enum CustomFieldType {
/**
* 输入框
*/
INPUT(false, "input"),
INPUT(false),
/**
* 文本框
*/
TEXTAREA(false, "textarea"),
TEXTAREA(false),
/**
* 单选下拉框框
*/
SELECT(true, "select"),
SELECT(true),
/**
* 多选下拉框框
*/
MULTIPLE_SELECT(true, "multipleSelect"),
MULTIPLE_SELECT(true),
/**
* 单选框
*/
RADIO(true, "radio"),
RADIO(true),
/**
* 复选框
*/
CHECKBOX(true, "checkbox"),
CHECKBOX(true),
/**
* 单选成员
*/
MEMBER(true, "member"),
MEMBER(true),
/**
* 多选成员
*/
MULTIPLE_MEMBER(true, "multipleMember"),
MULTIPLE_MEMBER(true),
/**
* 日期
*/
DATE(false, "date"),
DATE(false),
/**
* 日期时间
*/
DATETIME(false, "datetime"),
DATETIME(false),
/**
* 整型
*/
INT(false, "int"),
INT(false),
/**
* 浮点型
*/
FLOAT(false, "float"),
FLOAT(false),
/**
* 多值输入框标签输入框
*/
MULTIPLE_INPUT(false, "multipleInput");
MULTIPLE_INPUT(false);
private final Boolean hasOption;
private final String type;
CustomFieldType(Boolean hasOption, String type) {
CustomFieldType(Boolean hasOption) {
this.hasOption = hasOption;
this.type = type;
}
public Boolean getHasOption() {
return this.hasOption;
}
public String getType() {
return this.type;
}
public static Set<String> getHasOptionValueSet() {
return Arrays.stream(CustomFieldType.values())
.filter(CustomFieldType::getHasOption)

View File

@ -413,16 +413,16 @@
</if>
select api_id from api_definition_custom_field where field_id = #{custom.id}
<choose>
<when test="custom.type == 'richText' or custom.type == 'textarea' or custom.operator == 'current user'">
<when test="custom.type == 'TEXTAREA' or custom.operator == 'current user'">
and `value`
<include refid="io.metersphere.system.mapper.BaseMapper.condition">
<property name="object" value="custom"/>
</include>
</when>
<when test="custom.type == 'multipleMember' or custom.type == 'checkbox' or custom.type == 'multipleSelect'">
<when test="custom.type == 'MULTIPLE_MEMBER' or custom.type == 'CHECKBOX' or custom.type == 'MULTIPLE_SELECT'">
and ${custom.value}
</when>
<when test="custom.type == 'date' or custom.type == 'datetime'">
<when test="custom.type == 'DATE' or custom.type == 'DATETIME'">
and left(replace(unix_timestamp(trim(both '"' from `value`)), '.', ''), 13)
<include refid="io.metersphere.system.mapper.BaseMapper.condition">
<property name="object" value="custom"/>

View File

@ -266,16 +266,16 @@
</if>
select api_id from api_definition_custom_field where field_id = #{custom.id}
<choose>
<when test="custom.type == 'richText' or custom.type == 'textarea' or custom.operator == 'current user'">
<when test="custom.type == 'TEXTAREA' or custom.operator == 'current user'">
and `value`
<include refid="io.metersphere.system.mapper.BaseMapper.condition">
<property name="object" value="custom"/>
</include>
</when>
<when test="custom.type == 'multipleMember' or custom.type == 'checkbox' or custom.type == 'multipleSelect'">
<when test="custom.type == 'MULTIPLE_MEMBER' or custom.type == 'CHECKBOX' or custom.type == 'MULTIPLE_SELECT'">
and ${custom.value}
</when>
<when test="custom.type == 'date' or custom.type == 'datetime'">
<when test="custom.type == 'DATE' or custom.type == 'DATETIME'">
and left(replace(unix_timestamp(trim(both '"' from `value`)), '.', ''), 13)
<include refid="io.metersphere.system.mapper.BaseMapper.condition">
<property name="object" value="custom"/>

View File

@ -314,16 +314,16 @@
</if>
select api_id from api_definition_custom_field where field_id = #{custom.id}
<choose>
<when test="custom.type == 'richText' or custom.type == 'textarea' or custom.operator == 'current user'">
<when test="custom.type == 'TEXTAREA' or custom.operator == 'current user'">
and `value`
<include refid="io.metersphere.system.mapper.BaseMapper.condition">
<property name="object" value="custom"/>
</include>
</when>
<when test="custom.type == 'multipleMember' or custom.type == 'checkbox' or custom.type == 'multipleSelect'">
<when test="custom.type == 'MULTIPLE_MEMBER' or custom.type == 'CHECKBOX' or custom.type == 'MULTIPLE_SELECT'">
and ${custom.value}
</when>
<when test="custom.type == 'date' or custom.type == 'datetime'">
<when test="custom.type == 'DATE' or custom.type == 'DATETIME'">
and left(replace(unix_timestamp(trim(both '"' from `value`)), '.', ''), 13)
<include refid="io.metersphere.system.mapper.BaseMapper.condition">
<property name="object" value="custom"/>

View File

@ -944,13 +944,13 @@ public class ApiDefinitionControllerTests extends BaseTest {
Map<String, Object> custom = new HashMap<>();
custom.put("id", "test_field");
custom.put("operator", "in");
custom.put("type", "multipleSelect");
custom.put("type", "MULTIPLE_SELECT");
custom.put("value", JSON.toJSONString(List.of("test", "default")));
customs.add(custom);
Map<String, Object> currentUserCustom = new HashMap<>();
currentUserCustom.put("id", "test_field");
currentUserCustom.put("operator", "current user");
currentUserCustom.put("type", "multipleMember");
currentUserCustom.put("type", "MULTIPLE_MEMBER");
currentUserCustom.put("value", "current user");
customs.add(currentUserCustom);
map.put("customs", customs);

View File

@ -259,9 +259,9 @@ public class BugService {
detail.setPlatformDefault(template.getPlatformDefault());
detail.setStatus(bug.getStatus());
detail.setPlatformBugId(bug.getPlatformBugId());
detail.setTitle(bug.getTitle());
if (!detail.getPlatformDefault()) {
// 非平台默认模板 {标题, 内容, 标签, 自定义字段: 处理人, 状态}
detail.setTitle(bug.getTitle());
// 非平台默认模板 {内容, 标签, 自定义字段: 处理人, 状态}
BugContent bugContent = bugContentMapper.selectByPrimaryKey(id);
detail.setDescription(bugContent.getDescription());
detail.setTags(bug.getTags());
@ -286,7 +286,7 @@ public class BugService {
}
});
} else {
// 平台默认模板
// 平台默认模板 {自定义字段}
allCustomFields.forEach(field -> template.getCustomFields().stream().filter(templateField -> StringUtils.equals(templateField.getFieldId(), field.getId())).findFirst().ifPresent(templateField -> {
field.setName(templateField.getFieldName());
field.setType(templateField.getType());
@ -697,7 +697,7 @@ public class BugService {
handleUserField.setFieldId(BugTemplateCustomField.HANDLE_USER.getId());
handleUserField.setFieldName(BugTemplateCustomField.HANDLE_USER.getName());
handleUserField.setFieldKey(BugTemplateCustomField.HANDLE_USER.getId());
handleUserField.setType(CustomFieldType.SELECT.getType());
handleUserField.setType(CustomFieldType.SELECT.name());
List<SelectOption> localHandlerOption = bugCommonService.getLocalHandlerOption(projectId);
handleUserOption = localHandlerOption.stream().map(user -> {
CustomFieldOption option = new CustomFieldOption();
@ -713,7 +713,7 @@ public class BugService {
// 成员类型的自定义字段, 选项值与处理人选项保持一致
final List<CustomFieldOption> memberOption = handleUserOption;
templateDTO.getCustomFields().forEach(field -> {
if (StringUtils.equalsAny(field.getType(), CustomFieldType.MEMBER.getType(), CustomFieldType.MULTIPLE_MEMBER.getType())) {
if (StringUtils.equalsAny(field.getType(), CustomFieldType.MEMBER.name(), CustomFieldType.MULTIPLE_MEMBER.name())) {
field.setPlatformOptionJson(JSON.toJSONString(memberOption));
}
});
@ -737,7 +737,7 @@ public class BugService {
statusField.setFieldId(BugTemplateCustomField.STATUS.getId());
statusField.setFieldName(BugTemplateCustomField.STATUS.getName());
statusField.setFieldKey(BugTemplateCustomField.STATUS.getId());
statusField.setType(CustomFieldType.SELECT.getType());
statusField.setType(CustomFieldType.SELECT.name());
List<SelectOption> statusOption = bugStatusService.getToStatusItemOption(projectId, fromStatusId, platformBugKey);
List<CustomFieldOption> statusCustomOption = statusOption.stream().map(option -> {
CustomFieldOption customFieldOption = new CustomFieldOption();
@ -1252,6 +1252,8 @@ public class BugService {
customField.setFieldId(platformCustomField.getId());
customField.setFieldName(platformCustomField.getName());
customField.setPlatformOptionJson(platformCustomField.getOptions());
customField.setPlatformPlaceHolder(platformCustomField.getPlaceHolder());
customField.setPlatformSystemField(platformCustomField.getSystemField());
return customField;
}).collect(Collectors.toList());
template.setCustomFields(customFields);

View File

@ -588,7 +588,7 @@ public class BugControllerTests extends BaseTest {
BugCustomFieldDTO summary = new BugCustomFieldDTO();
summary.setId("summary");
summary.setName("摘要");
summary.setType("input");
summary.setType("INPUT");
summary.setValue("这是一个系统Jira模板创建的缺陷");
addRequest.getCustomFields().add(summary);
MultiValueMap<String, Object> addParam3 = getMultiPartParam(addRequest, null);
@ -667,7 +667,7 @@ public class BugControllerTests extends BaseTest {
field.setId("test_field");
field.setName("test");
field.setValue("test");
field.setType("multipleSelect");
field.setType("MULTIPLE_SELECT");
bugService.transferCustomToPlatformField(null, List.of(field), true);
// 添加没有配置自定义映射字段的Jira缺陷
removeApiFieldTmp();
@ -694,8 +694,6 @@ public class BugControllerTests extends BaseTest {
this.requestMultipart(BUG_ADD, addParam).andExpect(status().is5xxServerError());
// 获取禅道模板(删除默认项目模板)
bugService.attachTemplateStatusField(null, null, null, null);
// 获取处理人选项
this.requestGetWithOk(BUG_HEADER_COLUMNS_OPTION + "/default-project-for-bug");
// 批量删除
BugBatchRequest request = new BugBatchRequest();
request.setProjectId("default-project-for-bug");

View File

@ -66,7 +66,7 @@ INSERT INTO project_application (project_id, type, type_value) VALUES
INSERT INTO service_integration(`id`, `plugin_id`, `enable`, `configuration`, `organization_id`) VALUES
('621103810617344', 'jira', true, 0x504B0304140008080800BC517657000000000000000000000000030000007A6970258DC10EC2201044FF65CF06D2C498D89347B5574FBD6D8158222CD85D6268E3BF4BE3F5CDBC990DD0DAC531430FB348E65EEBE06B41AAA9289480CC1E4991130D07C022F3A366D7DA13B2373B32261592469AF1572FCF883E289362CB735BF8A4C5EE073474C3CB8E59A6F85EEFF12AE676EC4E67F8FE00504B0708384DA4307800000087000000504B01021400140008080800BC517657384DA43078000000870000000300000000000000000000000000000000007A6970504B0506000000000100010031000000A90000000000, '100001'),
('652096294625281', 'zentao', true, 0x504B03041400080808003B939C57000000000000000000000000030000007A6970AB564A4C49294A2D2E56B252CA282929B0D2D7373437D23334D33334D03333D3AF4ACD2B49CCD757D2514A4C4ECE2FCD2B01AA4B4CC9CDCC038A1424161797E717A500859C1373F2F3D21D8C0C0C4D811245A985A5A9C525219505A940B900C7108F784F3F377FA55A00504B07081BBBB5766A0000006E000000504B010214001400080808003B939C571BBBB5766A0000006E0000000300000000000000000000000000000000007A6970504B05060000000001000100310000009B0000000000, '100001');
('652096294625281', 'zentao', true, 0x504B030414000808080093756458000000000000000000000000030000007A6970AB564A4C49294A2D2E56B252CA282929B0D2D7373437D23334D3333230D033B3B4B230B0B050D2514A4C4ECE2FCD2B01AA4A4CC9CDCC038A1424161797E717A500859C1373F2F3D21D8C0C0C4D811245A985A5A9C525219505A940B900C7108F784F3F377FA55A00504B07088A813510680000006C000000504B01021400140008080800937564588A813510680000006C0000000300000000000000000000000000000000007A6970504B0506000000000100010031000000990000000000, '100001');

View File

@ -33,9 +33,6 @@ public class TemplateCustomFieldDTO {
@Schema(title = "选项值")
private List<CustomFieldOption> options;
@Schema(title = "平台选项值")
private String platformOptionJson;
@Schema(title = "是否支持搜索")
private Boolean supportSearch;
@ -44,4 +41,16 @@ public class TemplateCustomFieldDTO {
@Schema(description = "是否内置字段")
private Boolean internal;
/**
* 平台字段相关属性 -- start
*/
@Schema(title = "平台选项值")
private String platformOptionJson;
@Schema(description = "平台字段占位提示")
private String platformPlaceHolder;
@Schema(description = "是否平台系统字段")
private Boolean platformSystemField;
}

View File

@ -69,8 +69,8 @@ public class CustomFieldUtils {
String fieldType = custom.get(COMBINE_CUSTOM_FIELD_TYPE).toString();
String fieldValue = custom.get(COMBINE_CUSTOM_FIELD_VALUE).toString();
if (StringUtils.equalsAny(fieldType, CustomFieldType.MULTIPLE_MEMBER.getType(),
CustomFieldType.CHECKBOX.getType(), CustomFieldType.MULTIPLE_SELECT.getType()) && StringUtils.isNotEmpty(fieldValue)) {
if (StringUtils.equalsAny(fieldType, CustomFieldType.MULTIPLE_MEMBER.name(),
CustomFieldType.CHECKBOX.name(), CustomFieldType.MULTIPLE_SELECT.name()) && StringUtils.isNotEmpty(fieldValue)) {
List<String> customValues = JSON.parseArray(fieldValue, String.class);
List<String> jsonValues = customValues.stream().map(item -> "JSON_CONTAINS(`value`, '[\"".concat(item).concat("\"]')")).toList();
custom.put(COMBINE_CUSTOM_FIELD_VALUE, "(".concat(StringUtils.join(jsonValues, " OR ")).concat(")"));

View File

@ -38,13 +38,13 @@ class CustomFieldTests extends BaseTest {
Map<String, Object> custom = new HashMap<>();
custom.put("id", "test_field");
custom.put("operator", "in");
custom.put("type", "multipleSelect");
custom.put("type", "MULTIPLE_SELECT");
custom.put("value", JSON.toJSONString(List.of("test", "default")));
customs.add(custom);
Map<String, Object> currentUserCustom = new HashMap<>();
currentUserCustom.put("id", "test_field");
currentUserCustom.put("operator", "current user");
currentUserCustom.put("type", "multipleMember");
currentUserCustom.put("type", "MULTIPLE_MEMBER");
currentUserCustom.put("value", "current user");
customs.add(currentUserCustom);
currentUserCustom.put("value", "");

View File

@ -180,6 +180,16 @@ export const PASSWORD = {
},
};
export const CASCADER = {
type: 'cascader',
field: 'fieldName',
title: '',
value: [],
props: {
'allow-clear': true,
},
};
export const FieldTypeFormRules: Record<string, FormRule> = {
INPUT,
SELECT,
@ -196,4 +206,5 @@ export const FieldTypeFormRules: Record<string, FormRule> = {
TEXTAREA,
JIRAKEY,
PASSWORD,
CASCADER,
};

View File

@ -16,9 +16,8 @@
import { useI18n } from '@/hooks/useI18n';
import type { FormItem } from './types';
import { FormRuleItem } from './types';
import formCreate, { Rule } from '@form-create/arco-design';
import { FormItem, FormRuleItem } from './types';
import formCreate from '@form-create/arco-design';
defineOptions({ name: 'MsFormCreate' });
@ -147,6 +146,20 @@
formItems.value = formItems.value.filter((item: FormItem) => !item.displayConditions?.field);
}
function mapOption(options: any[]) {
return options.map((optionsItem) => {
const mappedItem: any = {
label: optionsItem.text,
value: optionsItem.value,
};
if (optionsItem.children) {
mappedItem.children = mapOption(optionsItem.children);
}
return mappedItem;
});
}
function convertItem(item: FormItem) {
//
let fieldType;
@ -162,12 +175,7 @@
fieldType = FieldTypeFormRules[currentTypeForm].type;
}
const options = item?.options;
const currentOptions = options?.map((optionsItem: any) => {
return {
label: optionsItem.text,
value: optionsItem.value,
};
});
const currentOptions = mapOption(options || []);
const ruleItem: any = {
type: fieldType, //
field: item.name, //
@ -206,6 +214,10 @@
tooltip: item.tooltip,
},
};
// placeholder, placeholder
if (item.platformPlaceHolder) {
ruleItem.props.placeholder = item.platformPlaceHolder;
}
// namelink
if (!ruleItem.link.filter((ink: string) => ink).length) {
delete ruleItem.link;

View File

@ -18,6 +18,7 @@ export type FormItemType =
| 'FLOAT'
| 'NUMBER'
| 'PassWord'
| 'CASCADER'
| undefined;
// 表单选项
@ -64,6 +65,7 @@ export interface FormItem {
inputSearch?: boolean; // 是否支持远程搜索
tooltip?: string; // 表单后边的提示info
instructionsIcon?: ''; // 是否有图片在表单后边展示
platformPlaceHolder?: string; // 平台表单项占位符
optionMethod?: string; // 请求检索的方法 两个参数 表单项的所有值
options?: FormItemDefaultOptions[];
required: boolean;

View File

@ -1,6 +1,6 @@
import {FormItemType} from '@/components/pure/ms-form-create/types';
import { FormItemType } from '@/components/pure/ms-form-create/types';
import {BatchApiParams} from './common';
import { BatchApiParams } from './common';
export interface BugListItem {
id: string; // 缺陷id
@ -47,6 +47,8 @@ export interface BugEditCustomField {
required: boolean;
isMutiple?: boolean;
options?: any[];
defaultValue: string;
platformSystemField: boolean;
}
export interface BugEditFormObject {
[key: string]: any;
@ -54,7 +56,7 @@ export interface BugEditFormObject {
export interface BugEditCustomFieldItem {
id: string;
name: string;
type: string;
type: string | undefined;
value: string;
}
export type BugBatchUpdateFiledType = 'single_select' | 'multiple_select' | 'tags' | 'input' | 'user_selector' | 'date';
@ -104,6 +106,6 @@ export interface OperationFile {
}
export interface BugTemplateRequest {
fromStatusId: string; // 缺陷当前状态
platformBugKey: string; // 缺陷第三方平台Key
fromStatusId: string; // 缺陷当前状态
platformBugKey: string; // 缺陷第三方平台Key
}

View File

@ -0,0 +1,29 @@
export interface Option {
label: string;
value: string;
children?: Option[];
}
// 递归函数,获取所有父级元素
export function findParents(data: Option[], targetId: string, parents: string[] = []) {
for (let i = 0; i < data.length; i++) {
const current = data[i];
if (current.value === targetId) {
// 第一层级, 直接返回
parents.push(current.value);
return parents;
}
if (current.children && current.children.length > 0) {
// 多层级, 递归查找
parents.push(current.value);
const result = findParents(current.children, targetId, parents);
if (result) {
// 子元素中有匹配的, 返回结果
return result;
}
// 子元素中没有匹配的, 队尾元素删除, 继续查找相邻元素
parents.pop();
}
}
return null;
}

View File

@ -100,6 +100,8 @@
:form-item="formItem"
:allow-edit="true"
:detail-info="detailInfo"
:is-platform-default-template="isPlatformDefaultTemplate"
:platform-system-fields="platformSystemFields"
@update-success="updateSuccess"
/>
@ -118,7 +120,11 @@
</template>
<template #second>
<div class="rightWrapper p-[24px]">
<div class="mb-4 font-medium">{{ t('bugManagement.detail.basicInfo') }}</div>
<div class="mb-4 font-medium">
<strong>
{{ t('bugManagement.detail.basicInfo') }}
</strong>
</div>
<!-- 自定义字段开始 -->
<MsFormCreate
v-if="formRules.length"
@ -131,7 +137,8 @@
@change="handelFormCreateChange"
/>
<!-- 自定义字段结束 -->
<div class="baseItem">
<!-- 内置基础信息开始 -->
<div v-if="!isPlatformDefaultTemplate" class="baseItem">
<a-form-item field="tags" :label="t('system.orgTemplate.tags')">
<MsTagsInput v-model:model-value="tags" />
</a-form-item>
@ -140,6 +147,7 @@
<!-- <MsTag v-for="item of tags" :key="item"> {{ item }} </MsTag>-->
<!-- </span>-->
</div>
<!-- 内置基础信息结束 -->
</div>
</template>
</MsSplitBox>
@ -226,17 +234,19 @@
const currentProjectId = computed(() => appStore.currentProjectId);
const showDrawerVisible = defineModel<boolean>('visible', { default: false });
const bugDetailTabRef = ref();
const isPlatformDefaultTemplate = ref(false);
const activeTab = ref<string>('detail');
const detailInfo = ref<Record<string, any>>({ match: [] }); // loadBug
const tags = ref([]);
const platformSystemFields = ref<BugEditCustomField[]>([]); //
//
const getFormRules = (arr: BugEditCustomField[], valueObj: BugEditFormObject) => {
formRules.value = [];
if (Array.isArray(arr) && arr.length) {
formRules.value = arr.map((item) => {
formRules.value = arr.map((item: any) => {
return {
type: item.type,
name: item.fieldId,
@ -244,6 +254,7 @@
value: valueObj[item.fieldId],
options: item.platformOptionJson ? JSON.parse(item.platformOptionJson) : item.options,
required: item.required as boolean,
platformPlaceHolder: item.platformPlaceHolder,
props: {
modelValue: valueObj[item.fieldId],
options: item.platformOptionJson ? JSON.parse(item.platformOptionJson) : item.options,
@ -262,7 +273,14 @@
fromStatusId: request.fromStatusId,
platformBugKey: request.platformBugKey,
});
getFormRules(res.customFields, valueObj);
platformSystemFields.value = res.customFields.filter((field) => field.platformSystemField);
platformSystemFields.value.forEach((item) => {
item.defaultValue = valueObj[item.fieldId];
});
getFormRules(
res.customFields.filter((field) => !field.platformSystemField),
valueObj
);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
@ -272,16 +290,24 @@
async function loadedBug(detail: BugEditFormObject) {
detailInfo.value = { ...detail };
const { templateId } = detail;
// tag
//
isPlatformDefaultTemplate.value = detail.platformDefault;
// TAG
tags.value = detail.tags || [];
caseCount.value = detail.linkCaseCount;
const tmpObj = {};
const tmpObj = { status: detail.status };
if (detail.customFields && Array.isArray(detail.customFields)) {
detail.customFields.forEach((item) => {
if (item.type === 'MULTIPLE_SELECT') {
if (item.type === 'MULTIPLE_SELECT' || item.type === 'MULTIPLE_INPUT' || item.type === 'CHECKBOX') {
tmpObj[item.id] = JSON.parse(item.value);
} else if (item.type === 'INT') {
tmpObj[item.id] = Number(item.value);
} else if (item.type === 'CASCADER') {
const arr = JSON.parse(item.value);
if (arr && arr instanceof Array && arr.length > 0) {
tmpObj[item.id] = arr[arr.length - 1];
}
} else {
tmpObj[item.id] = item.value;
}

View File

@ -1,7 +1,6 @@
<template>
<div class="relative p-[16px] pb-[16px]">
<div class="header">
<div class="header-title">{{ t('bugManagement.edit.content') }}</div>
<div v-permission="['PROJECT_BUG:READ+UPDATE']" class="header-action">
<a-button type="text" @click="contentEditAble = !contentEditAble">
<template #icon> <MsIconfont type="icon-icon_edit_outlined" /> </template>
@ -9,24 +8,53 @@
</a-button>
</div>
</div>
<div class="mt-[16px]" :class="{ 'max-h-[260px]': contentEditAble }">
<MsRichText
v-if="contentEditAble"
v-model:raw="form.description"
v-model:filed-ids="fileIds"
:disabled="!contentEditAble"
:placeholder="t('bugManagement.edit.contentPlaceholder')"
:upload-image="handleUploadImage"
/>
<div v-else v-dompurify-html="form?.description || '-'" class="markdown-body"></div>
<!-- 左侧布局默认内容(非平台默认模板时默认展示) -->
<div v-if="!isPlatformDefaultTemplate" class="default-content">
<div class="header-title">{{ t('bugManagement.edit.content') }}</div>
<div class="mb-4 mt-[16px]" :class="{ 'max-h-[260px]': contentEditAble }">
<MsRichText
v-if="contentEditAble"
v-model:raw="form.description"
v-model:filed-ids="fileIds"
:disabled="!contentEditAble"
:placeholder="t('bugManagement.edit.contentPlaceholder')"
:upload-image="handleUploadImage"
/>
<div v-else v-dompurify-html="form?.description || '-'" class="markdown-body"></div>
</div>
<div v-if="contentEditAble" class="mt-[8px] flex justify-end">
<a-button type="secondary" @click="handleCancel">{{ t('common.cancel') }}</a-button>
<a-button class="ml-[12px]" type="primary" :loading="confirmLoading" @click="handleSave">
{{ t('common.save') }}
</a-button>
</div>
</div>
<div v-if="contentEditAble" class="mt-[8px] flex justify-end">
<a-button type="secondary" @click="handleCancel">{{ t('common.cancel') }}</a-button>
<a-button class="ml-[12px]" type="primary" :loading="confirmLoading" @click="handleSave">
{{ t('common.save') }}
</a-button>
<!-- 特殊布局内容(平台默认模板时展示) -->
<div v-if="isPlatformDefaultTemplate" class="special-content">
<div v-for="(item, index) in platformSystemFields" :key="index">
<div v-if="item.fieldId !== 'summary'">
<h1 class="header-title">
<strong>{{ item.fieldName }}</strong>
</h1>
<div class="mb-4 mt-[16px]" :class="{ 'max-h-[260px]': contentEditAble }">
<MsRichText
v-if="contentEditAble"
v-model:raw="item.defaultValue"
:disabled="!contentEditAble"
:placeholder="t('bugManagement.edit.contentPlaceholder')"
/>
<div v-else v-dompurify-html="item?.defaultValue || '-'" class="markdown-body"></div>
</div>
</div>
</div>
<div v-if="contentEditAble" class="mt-[8px] flex justify-end">
<a-button type="secondary" @click="handleCancel">{{ t('common.cancel') }}</a-button>
<a-button class="ml-[12px]" type="primary" :loading="confirmLoading" @click="handleSave">
{{ t('common.save') }}
</a-button>
</div>
</div>
<div style="margin-top: 20px">
<div class="mt-4">
<AddAttachment v-model:file-list="fileList" @link-file="associatedFile" />
</div>
<MsFileList
@ -180,8 +208,9 @@
import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store';
import { downloadByteFile, sleep } from '@/utils';
import { findParents, Option } from '@/utils/recursion';
import { BugEditCustomFieldItem, BugEditFormObject } from '@/models/bug-management';
import { BugEditCustomField, BugEditCustomFieldItem, BugEditFormObject } from '@/models/bug-management';
import { AssociatedList, AttachFileInfo } from '@/models/caseManagement/featureCase';
import { TableQueryParams } from '@/models/common';
@ -197,6 +226,8 @@
detailInfo: BugEditFormObject;
formItem: FormRuleItem[];
allowEdit?: boolean; //
isPlatformDefaultTemplate: boolean; //
platformSystemFields: BugEditCustomField[]; //
}>();
const emit = defineEmits<{
@ -401,21 +432,42 @@
const customFields: BugEditCustomFieldItem[] = [];
if (formItem && formItem.length) {
formItem.forEach((item: FormRuleItem) => {
let itemVal = item.value;
if (item.sourceType === 'CASCADER') {
itemVal = findParents(item.options as Option[], item.value as string, []);
}
customFields.push({
id: item.field as string,
name: item.title as string,
type: item.sourceType as string,
value: Array.isArray(item.value) ? JSON.stringify(item.value) : (item.value as string),
value: Array.isArray(itemVal) ? JSON.stringify(itemVal) : (itemVal as string),
});
});
}
if (props.isPlatformDefaultTemplate) {
//
props.platformSystemFields.forEach((item) => {
customFields.push({
id: item.fieldId,
name: item.fieldName,
type: item.type,
value: item.defaultValue,
});
});
}
const tmpObj: BugEditFormObject = {
...form.value,
id: props.detailInfo.id,
projectId: currentProjectId.value,
templateId: props.detailInfo.templateId,
deleteLocalFileIds: form.value.deleteLocalFileIds,
unLinkRefIds: form.value.unLinkRefIds,
linkFileIds: form.value.linkFileIds,
customFields,
};
if (!props.isPlatformDefaultTemplate) {
tmpObj.description = form.value.description;
tmpObj.title = form.value.title;
}
//
const res = await createOrUpdateBug({ request: tmpObj, fileList: [] as unknown as File[] });
if (res) {

View File

@ -24,7 +24,9 @@
<a-form ref="formRef" :model="form" layout="vertical">
<div class="flex flex-row">
<div class="left mt-[16px] min-w-[732px] grow pl-[24px]">
<!-- 平台默认模板不展示缺陷名称, 描述 -->
<a-form-item
v-if="!isPlatformDefaultTemplate"
field="title"
:label="t('bugManagement.bugName')"
:rules="[{ required: true, message: t('bugManagement.edit.nameIsRequired') }]"
@ -32,13 +34,38 @@
>
<a-input v-model="form.title" :max-length="255" />
</a-form-item>
<a-form-item field="description" :label="t('bugManagement.edit.content')">
<a-form-item v-if="!isPlatformDefaultTemplate" field="description" :label="t('bugManagement.edit.content')">
<MsRichText
v-model:raw="form.description"
v-model:filed-ids="richTextFileIds"
:upload-image="handleUploadImage"
/>
</a-form-item>
<!-- 平台默认模板展示字段, 暂时支持输入框, 富文本类型 -->
<div v-if="isPlatformDefaultTemplate">
<a-form-item
v-for="(value, key) in form.platformSystemFields"
:key="key"
:field="'platformSystemFields.' + key"
:label="platformSystemFieldMap[key].fieldName"
:rules="[
{
required: platformSystemFieldMap[key].required,
message: `${platformSystemFieldMap[key].fieldName}` + t('bugManagement.edit.cannotBeNull'),
},
]"
>
<a-input
v-if="platformSystemFieldMap[key].type === 'INPUT'"
v-model="form.platformSystemFields[key]"
:max-length="255"
/>
<MsRichText
v-if="platformSystemFieldMap[key].type === 'RICH_TEXT'"
v-model:raw="form.platformSystemFields[key]"
/>
</a-form-item>
</div>
<a-form-item field="attachment">
<div class="flex flex-col">
<div class="mb-1">
@ -124,7 +151,8 @@
<div class="right mt-[16px] max-w-[433px] grow pr-[24px]">
<div style="min-width: 250px; overflow: auto">
<MsFormCreate ref="formCreateRef" v-model:formItem="formItem" v-model:api="fApi" :form-rule="formRules" />
<a-form-item field="tag" :label="t('bugManagement.tag')">
<!-- 平台默认模板不展示标签, 与第三方保持一致 -->
<a-form-item v-if="!isPlatformDefaultTemplate" field="tag" :label="t('bugManagement.tag')">
<MsTagsInput
v-model:model-value="form.tags"
:placeholder="t('bugManagement.edit.tagPlaceholder')"
@ -200,6 +228,7 @@
import { useAppStore } from '@/store';
import { downloadByteFile } from '@/utils';
import { scrollIntoView } from '@/utils/dom';
import { findParents, Option } from '@/utils/recursion';
import {
BugEditCustomField,
@ -237,8 +266,13 @@
deleteLocalFileIds: [],
unLinkRefIds: [],
linkFileIds: [],
//
platformSystemFields: {},
});
//
const platformSystemFieldMap = {};
const getListFunParams = ref<TableQueryParams>({
combine: {
hiddenIds: [],
@ -262,6 +296,7 @@
const bugId = computed(() => route.query.id || '');
const isEditOrCopy = computed(() => !!bugId.value);
const isCopy = computed(() => route.params.mode === 'copy');
const isPlatformDefaultTemplate = ref(false);
const imageUrl = ref('');
const previewVisible = ref<boolean>(false);
const richTextFileIds = ref<string[]>([]);
@ -323,6 +358,7 @@
value: item.defaultValue,
options: item.platformOptionJson ? JSON.parse(item.platformOptionJson) : item.options,
required: item.required as boolean,
platformPlaceHolder: item.platformPlaceHolder,
props: {
modelValue: item.defaultValue,
options: item.platformOptionJson ? JSON.parse(item.platformOptionJson) : item.options,
@ -335,12 +371,22 @@
const templateChange = async (v: SelectValue, request?: BugTemplateRequest) => {
if (v) {
try {
loading.value = true;
let param = { projectId: appStore.currentProjectId, id: v };
if (request) {
param = { ...param, ...request };
}
const res = await getTemplateById(param);
getFormRules(res.customFields);
isPlatformDefaultTemplate.value = res.platformDefault;
if (isPlatformDefaultTemplate.value) {
const systemFields = res.customFields.filter((field) => field.platformSystemField);
systemFields.forEach((field) => {
form.value.platformSystemFields[field.fieldId] = field.defaultValue;
platformSystemFieldMap[field.fieldId] = field;
});
}
getFormRules(res.customFields.filter((field) => !field.platformSystemField));
loading.value = false;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
@ -461,6 +507,9 @@
const customFields: BugEditCustomFieldItem[] = [];
if (formItem.value && formItem.value.length) {
formItem.value.forEach((item: FormRuleItem) => {
if (item.sourceType === 'CASCADER') {
item.value = findParents(item.options as Option[], item.value as string, []);
}
customFields.push({
id: item.field as string,
name: item.title as string,
@ -469,6 +518,21 @@
});
});
}
if (isPlatformDefaultTemplate.value && form.value.platformSystemFields) {
Object.keys(form.value.platformSystemFields).forEach((key) => {
customFields.push({
id: platformSystemFieldMap[key].fieldId,
name: platformSystemFieldMap[key].fieldName,
type: platformSystemFieldMap[key].type,
value: form.value.platformSystemFields[key],
});
});
delete form.value.platformSystemFields;
// , ,
delete form.value.title;
delete form.value.description;
delete form.value.tags;
}
//
const copyFileList = fileList.value.filter((item) => item.isCopyFlag);
let copyFiles: { refId: string; fileId: string; local: boolean }[] = [];
@ -550,6 +614,7 @@
};
//
const getDetailInfo = async () => {
loading.value = true;
const id = route.query.id as string;
if (!id) return;
const res = await getBugDetail(id);
@ -580,16 +645,24 @@
});
}
const tmpObj = {};
let tmpObj = {};
if (isEdit.value) {
tmpObj = { status: res.status };
}
if (customFields && Array.isArray(customFields)) {
customFields.forEach((item) => {
if (item.id === 'status' && isCopy.value) {
// ,
tmpObj[item.id] = '';
} else if (item.type === 'MULTIPLE_SELECT') {
} else if (item.type === 'MULTIPLE_SELECT' || item.type === 'MULTIPLE_INPUT' || item.type === 'CHECKBOX') {
tmpObj[item.id] = JSON.parse(item.value);
} else if (item.type === 'INT') {
tmpObj[item.id] = Number(item.value);
} else if (item.type === 'CASCADER') {
const arr = JSON.parse(item.value);
if (arr && arr instanceof Array && arr.length > 0) {
tmpObj[item.id] = arr[arr.length - 1];
}
} else {
tmpObj[item.id] = item.value;
}
@ -597,6 +670,13 @@
}
//
fApi.value.setValue(tmpObj);
//
if (isPlatformDefaultTemplate && form.value.platformSystemFields) {
Object.keys(form.value.platformSystemFields).forEach((key) => {
form.value.platformSystemFields[key] = tmpObj[key];
});
}
const { platformSystemFields } = form.value;
//
form.value = {
id: res.id,
@ -605,7 +685,9 @@
templateId: res.templateId,
tags: res.tags,
projectId: res.projectId,
platformSystemFields,
};
loading.value = false;
};
const initDefaultFields = async () => {

View File

@ -378,7 +378,7 @@
{
title: 'bugManagement.status',
dataIndex: 'statusName',
width: 84,
width: 100,
slotName: 'status',
titleSlotName: 'statusFilter',
showDrag: true,
@ -390,7 +390,7 @@
slotName: 'handleUser',
showTooltip: true,
titleSlotName: 'handleUserFilter',
width: 75,
width: 100,
showDrag: true,
showInTable: true,
},
@ -404,7 +404,7 @@
},
{
title: 'bugManagement.belongPlatform',
width: 135,
width: 100,
showDrag: true,
dataIndex: 'platform',
showInTable: true,
@ -421,7 +421,7 @@
title: 'bugManagement.creator',
dataIndex: 'createUser',
slotName: 'createUser',
width: 112,
width: 125,
showTooltip: true,
showDrag: true,
titleSlotName: 'createUserFilter',
@ -445,7 +445,7 @@
{
title: 'bugManagement.updateUser',
dataIndex: 'updateUser',
width: 112,
width: 125,
showTooltip: true,
showDrag: true,
titleSlotName: 'updateUserFilter',

View File

@ -53,6 +53,7 @@ export default {
linkFile: 'Link file',
contentEdit: 'Content edit',
linkCase: 'Link case',
cannotBeNull: 'is empty',
},
detail: {
title: '【{id}】{name}',

View File

@ -53,6 +53,7 @@ export default {
linkFile: '关联文件',
contentEdit: '内容编辑',
linkCase: '关联用例',
cannotBeNull: '不能为空',
},
detail: {
title: '【{id}】{name}',