73b16ac8 by xukangle

Merge branch 'develop' into dev_20241202_xukangle

2 parents a04a7e36 6181cbd2
......@@ -19,9 +19,9 @@ VITE_SERVE_BASE = /
# 流程设计访问地址
VITE_BPMN_URL = https://workflow-swzl-test.csbr.cn
# 数据标准、元数据、数据目录 接口地址
VITE_APP_PLAN_BASEURL = ms-daop-zcgl-data-plan-service
VITE_APP_PLAN_BASEURL = ms-daop-jgjf-data-plan-service
#数据质量接口地址
VITE_APP_QUALITY_BASEURL = ms-daop-zcgl-data-quality-service
VITE_APP_QUALITY_BASEURL = ms-daop-data-quality-service
#数据盘点接口地址
VITE_APP_CHECK_BASEURL = ms-daop-zcgl-data-inventory
......
......@@ -18,9 +18,9 @@ VITE_SERVE_BASE = /
# 流程设计访问地址
VITE_BPMN_URL = https://workflow.zgsjzc.com
# 数据标准、元数据、数据目录 接口地址
VITE_APP_PLAN_BASEURL = ms-daop-zcgl-data-plan-service
VITE_APP_PLAN_BASEURL = ms-daop-jgjf-data-plan-service
#数据质量接口地址
VITE_APP_QUALITY_BASEURL = ms-daop-zcgl-data-quality-service
VITE_APP_QUALITY_BASEURL = ms-daop-data-quality-service
#门户接口
VITE_API_PORTALURL = https://www.zgsjzc.com/portal
......
......@@ -175,28 +175,23 @@ export const deleteImportData = (params) => request({
})
// 数据导入-分页查询
export const getImportData = (params) => request({
url: `${import.meta.env.VITE_APP_ADD_FILE}/import-data/list`,
url: `${import.meta.env.VITE_APP_ADD_FILE}/import-data/page-list`,
method: 'post',
data: params,
})
// 导出数据字典模板
export const exportDictionary = (params) => request({
url: `${import.meta.env.VITE_APP_PLAN_BASEURL}/export/data-dictionary/schema`,
// 导出采集任务分类模板。
export const exportCollectTask = (params) => request({
url: `${import.meta.env.VITE_APP_ADD_FILE}/import-config/export-more-sheet-template`,
method: 'post',
data: params,
responseType: 'blob'
})
/** 获取树形结构的标准集列表 */
export const getStandardSetTree = () => request({
url: `${import.meta.env.VITE_APP_PLAN_BASEURL}/data-standard-set/list-tree`,
method: 'get'
})
// 数据字典树形数据
export const getDictionaryTree = (params) => request({
url: `${import.meta.env.VITE_APP_PLAN_BASEURL}/data-dictionary-general/tree-list`,
// 导出数据字典模板
export const exportDictionary = (params) => request({
url: `${import.meta.env.VITE_APP_PLAN_BASEURL}/export/data-dictionary/schema`,
method: 'post',
params
data: params,
responseType: 'blob'
})
\ No newline at end of file
......
......@@ -162,7 +162,7 @@ onBeforeMount(() => {
});
const getDefaultLargeCategory = (ruleCode) => {
if (ruleCode == 'repeate_data_check' || ruleCode == 'logic_check') {
if (ruleCode == 'repeate_data_check' || ruleCode == 'logic_check' || ruleCode == 'value_of_range') {
return '3'; //准确性
} else if (ruleCode == 'volatility_check' || ruleCode == 'rows_check') {
return '5';
......@@ -176,7 +176,7 @@ const getDefaultLargeCategory = (ruleCode) => {
const getDefaultSmallCategory = (ruleCode) => {
if (ruleCode == 'repeate_data_check') {
return '11'; //准确性
} else if (ruleCode == 'logic_check') {
} else if (ruleCode == 'logic_check' || ruleCode == 'value_of_range') {
return '9';
} else if (ruleCode == 'volatility_check' || ruleCode == 'rows_check') {
return '17';
......@@ -372,6 +372,17 @@ const panelList: any = ref([
block: true,
visible: false
}, {
label: '规则设置',
type: 'input-dom',
placeholder: '请设置,可多选',
field: 'ruleSettings-value-range', //12
default: '',//描述型的字段
defaultValue: {},// json,key为表, 实际的值域表格数据值数组
readonly: true,
required: true,
block: true,
visible: false
}, {
label: '描述',
type: 'textarea',
placeholder: '请输入',
......@@ -454,6 +465,9 @@ const formRules = ref({
'ruleSettings-rows': [
{ required: true, trigger: 'change', message: "请设置规则" }
],
'ruleSettings-value-range': [
{ required: true, trigger: 'change', message: "请设置规则" }
],
orangeThreshold: [
{
trigger: 'blur', validator: (rule: any, value: any, callback: any) => {
......@@ -651,6 +665,44 @@ const formBtnClick = (btn) => {
differenceRange: 0
}];
}
} else if (ruleType.value == 'value_of_range') {
valueRangeDialogVisible.value = true;
tableListInfo.value.data = props.toSubjectTables;
dialogSelectSubjectTable.value = props.toSubjectTables[0];
let defaultValue = panelList.value[12].defaultValue;
valueRangeTableListData.value[dialogSelectSubjectTable.value.enName] = [];
if (props.toSubjectTables[0]?.guid) {
valueRangeTableListLoading.value[dialogSelectSubjectTable.value.enName] = true;
getSubjectFields(props.toSubjectTables[0]?.guid).then((res: any) => {
valueRangeTableListLoading.value[dialogSelectSubjectTable.value.enName] = false;
if (res.code == proxy.$passCode) {
let data = res.data || [];
valueRangeTableListData.value[dialogSelectSubjectTable.value.enName] = data;
let valueFields = defaultValue.ruleFields?.[dialogSelectSubjectTable.value.enName] || [];
if (valueFields.length) {
valueFields.forEach(field => {
let fIndex = data.findIndex(d => d.enName == field.enName);
if (fIndex > -1) {
let f = valueRangeTableListData.value[dialogSelectSubjectTable.value.enName][fIndex];
if (field.dataRange) {
f.dataRange = field.dataRange;
}
if (field.startValue) {
if (f.dataType == 'date' || f.dataType == 'datetime') {
f.dateValueRange = [field.startValue, field.endValue];
} else {
f.startValue = field.startValue;
f.endValue = field.endValue;
}
}
}
});
}
} else {
ElMessage.error(res.msg);
}
})
}
}
}
......@@ -762,6 +814,22 @@ const setPanelListValue = (item, isSelectChange = false, init = false, radioGrou
} else {
p.defaultValue = [];
}
} else if (ruleCode == 'value_of_range' && p.field == 'ruleSettings-value-range') {
if (!init) {
p.default = val[field];
return;
}
if (val.ruleField) {
p.default = val.ruleField?.map(f => f.enName)?.join(';');
let ruleFields = {};
ruleFields[val.subjectName] = val.ruleField || [];
p.defaultValue = {
ruleFields: ruleFields
}
} else {
p.default = '';
p.defaultValue = {};
}
} else if (p.field == 'largeCategory') {
/** 此处有歧义,若是切换规则类型,修改默认值,可能会出现,用户先修改了规则大类,但是切换类型之后,被我还原了。 */
if (radioGroupChange && !init) {
......@@ -805,54 +873,54 @@ const radioGroupChange = (val, inlineValue, init) => {
list.forEach((item, index) => {
if (val == 'volatility_check') {//表行数波动率
item.visible = true
if (index == 7 || index == 8 || index == 9 || index == 10 || index == 11 || index === 13) {// 7是规则设置,9是联合不为空 : 9+4
if (index == 7 || index == 8 || index == 9 || index == 10 || index == 11 || index == 12 || index === 14) {// 7是规则设置,9是联合不为空 : 9+4
item.visible = false
}
} else if (val === 'null_value_check') {//空值检查
if (index === 3 || index === 4 || index === 5 || index === 6 || index == 7 || index == 8 || index == 9 || index == 11) {
if (index === 3 || index === 4 || index === 5 || index === 6 || index == 7 || index == 8 || index == 9 || index == 11 || index == 12) {
item.visible = false
} else { //index为10的显示
item.visible = true;
}
} else if (val === 'repeate_data_check') { //重复数据检查
item.visible = false
if (index === 3 || index === 4 || index === 5 || index === 6 || index == 8 || index == 9 || index == 10 || index == 11 || index === 13) {
if (index === 3 || index === 4 || index === 5 || index === 6 || index == 8 || index == 9 || index == 10 || index == 11 || index == 12 || index === 14) {
} else {
item.visible = true;
}
} else if (val === 'logic_check') { //逻辑检查
item.visible = false
if (index === 3 || index === 4 || index === 5 || index === 6 || index == 7 || index == 9 || index == 10 || index == 11 || index === 13) {
if (index === 3 || index === 4 || index === 5 || index === 6 || index == 7 || index == 9 || index == 10 || index == 11 || index == 12 || index === 14) {
} else {
item.visible = true;
}
} else if (val === 'value_of_range') { // 值域范围
item.visible = false
if (index === 3 || index === 4 || index === 5 || index === 6 || index == 7 || index == 9 || index == 10 || index == 11 || index === 13) {
if (index === 3 || index === 4 || index === 5 || index === 6 || index == 7 || index == 8 || index == 9 || index == 10 || index == 11 || index === 14) {
} else {
item.visible = true;
}
} else if (val === 'ref_integrality') { // 引用完整性
item.visible = false
if (index === 3 || index === 4 || index === 5 || index === 6 || index == 7 || index == 9 || index == 10 || index == 11 || index === 13) {
if (index === 3 || index === 4 || index === 5 || index === 6 || index == 7 || index == 8 || index == 9 || index == 10 || index == 11 || index === 14) {
} else {
item.visible = true;
}
} else if (val === 'norm_check') { // 规范校验
item.visible = false
if (index === 3 || index === 4 || index === 5 || index === 6 || index == 7 || index == 9 || index == 10 || index == 11 || index === 13) {
if (index === 3 || index === 4 || index === 5 || index === 6 || index == 7 || index == 8 || index == 9 || index == 10 || index == 11 || index === 13) {
} else {
item.visible = true;
}
} else if (val === 'custom_sql') {//自定义sql
item.visible = false
if (index === 3 || index === 4 || index === 5 || index === 6 || index == 7 || index == 8 || index == 10 || index == 11 || index === 13) {
if (index === 3 || index === 4 || index === 5 || index === 6 || index == 7 || index == 8 || index == 10 || index == 11 || index == 12 || index === 14) {
} else {
item.visible = true;
}
} else if (val === 'rows_check') { //表行数
item.visible = false
if (index === 3 || index === 4 || index === 5 || index === 6 || index == 7 || index == 8 || index == 9 || index == 10 || index === 13) {
if (index === 3 || index === 4 || index === 5 || index === 6 || index == 7 || index == 8 || index == 9 || index == 10 || index == 12 || index === 14) {
} else {
item.visible = true;
}
......@@ -916,6 +984,41 @@ const listItemClick = (data) => {
ElMessage.error(res.msg);
}
})
} else if (ruleType.value == 'value_of_range') {
if (valueRangeTableListData.value[dialogSelectSubjectTable.value.enName]?.length) {
return;
}
let defaultValue = panelList.value[12].defaultValue;
valueRangeTableListLoading.value[dialogSelectSubjectTable.value.enName] = true;
getSubjectFields(data.guid).then((res: any) => {
valueRangeTableListLoading.value[dialogSelectSubjectTable.value.enName] = false;
if (res.code == proxy.$passCode) {
let data = res.data || [];
valueRangeTableListData.value[dialogSelectSubjectTable.value.enName] = data;
let valueFields = defaultValue.ruleFields?.[dialogSelectSubjectTable.value.enName] || [];
if (valueFields.length) {
valueFields.forEach(field => {
let fIndex = data.findIndex(d => d.enName == field.enName);
if (fIndex > -1) {
let f = valueRangeTableListData.value[dialogSelectSubjectTable.value.enName][fIndex];
if (field.dataRange) {
f.dataRange = field.dataRange;
}
if (field.startValue) {
if (f.dataType == 'date' || f.dataType == 'datetime') {
f.dateValueRange = [field.startValue, field.endValue];
} else {
f.startValue = field.startValue;
f.endValue = field.endValue;
}
}
}
});
}
} else {
ElMessage.error(res.msg);
}
})
}
}
......@@ -1386,6 +1489,19 @@ const getSubjectTableByDomainData = (guid) => {
})
}
const defaultExpandedKeys = computed(() => {
return props.value && contrastSubjects.value?.length ? [contrastSubjects.value.find(c => c.children?.some(cc => cc.guid == props.value.contrastSubjectDomainGuid)).guid, props.value.contrastSubjectDomainGuid] : [];
})
const contrastSubjectInputFilterMethod = (v, data) => {
if (!v) {
return true;
}
return data.label?.includes(v) || data.name?.includes(v) ||
data.chName?.includes(v) ||
data.enName?.includes(v);
};
/** 主题表懒加载。 */
const treeSelectLoad = (node, resolve) => {
if (node.level == 0) {
......@@ -1401,18 +1517,73 @@ const treeSelectLoad = (node, resolve) => {
}
}
const defaultExpandedKeys = computed(() => {
return props.value && contrastSubjects.value?.length ? [contrastSubjects.value.find(c => c.children?.some(cc => cc.guid == props.value.contrastSubjectDomainGuid)).guid, props.value.contrastSubjectDomainGuid] : [];
})
/** --------------------- 值域范围 ----------------------------- */
const contrastSubjectInputFilterMethod = (v, data) => {
if (!v) {
return true;
const valueRangeDialogVisible = ref(false);
const valueRangeTableListLoading = ref({});
const valueRangeTableListData = ref({});
const cancelValueRangeDialog = () => {
valueRangeDialogVisible.value = false;
}
const submitValueRange = () => {
let v: any = [];
let ruleFieldsJson: any = {};
for (const table in valueRangeTableListData.value) {
if (!valueRangeTableListData.value[table]?.length) {
continue;
}
let valueTableFields = valueRangeTableListData.value[table];
let ruleFields: any = []
for (const field of valueTableFields) {
if (field.startValue != null && field.endValue == null || (field.endValue != null && field.startValue == null)) {
ElMessage.error(`表【${table}】的字段【${field.enName}】设置了值域,但范围未填写完整`);
return;
}
return data.label?.includes(v) || data.name?.includes(v) ||
data.chName?.includes(v) ||
data.enName?.includes(v);
};
if (field.startValue) {
ruleFields.push(field);
}
if (field.dataRange) {
ruleFields.push(field);
}
if (field.dateValueRange?.length > 0) {
field.startValue = field.dateValueRange[0];
field.endValue = field.dateValueRange[1];
ruleFields.push(field);
}
if (ruleFields.length) {
v.push(table);
ruleFieldsJson[table] = ruleFields;
}
}
}
if (!v.length) {
ElMessage.error('当前未给表字段设置值域!');
return;
}
let index = 12;
panelList.value[index].defaultValue = {
ruleFields: ruleFieldsJson
};
let str = "";
for (const key in ruleFieldsJson) {
let field = ruleFieldsJson[key];
str = str + (str ? ';' : '') + field.map(f => f.enName).join(',');;
}
let formInline = oldOriginValue.value = Object.assign({
qualityModelGuids: props.toSubjectTables.map(s => s.guid),
parity: 1,
compareWay: 1,
jointly: 'N',
bizState: 'Y'
}, oldOriginValue.value, ruleFormRef.value.formInline);
formInline[`${panelList.value[index].field}`] = str;
setPanelListValue(formInline);
valueRangeDialogVisible.value = false;
}
const getFormInfo = () => {
let formInline = ruleFormRef.value.formInline;
......@@ -1447,6 +1618,11 @@ const getFormInfo = () => {
rows: v,
ruleName: ruleName
});
} else if (formInline.ruleCode == 'value_of_range') {
let v = panelList.value[12].defaultValue;
return Object.assign({}, formInline, v, {
ruleName: ruleName
});
}
}
......@@ -1577,7 +1753,64 @@ defineExpose({
</el-dialog>
<!-- 值域范围 -->
<el-dialog></el-dialog>
<el-dialog v-model="valueRangeDialogVisible" title="规则设置" width="850" :modal="true" :close-on-click-modal="false"
destroy-on-close align-center>
<div class="filter-dialog-content" :style="{ height: '500px' }">
<div class="filter-table-list">
<div class="left-title">质检表</div>
<ListPanel class="list_unit" ref="valueCheckFormListRef" :listInfo="tableListInfo" @itemClick="listItemClick" />
</div>
<div class="table-field-right">
<div class="left-title">字段列表详情</div>
<el-table ref="rowTableRef" :data="valueRangeTableListData[dialogSelectSubjectTable.enName]" height="100%" :highlight-current-row="true" stripe
v-loading="valueRangeTableListLoading[dialogSelectSubjectTable.enName]" tooltip-effect="light" border
:style="{ height: 'calc(100% - 32px)', width: '100%', display: 'inline-block' }">
<el-table-column prop="enName" label="字段名" width="140px" align="left" show-overflow-tooltip>
</el-table-column>
<el-table-column prop="chName" label="注释" width="120px" align="left" show-overflow-tooltip>
</el-table-column>
<el-table-column prop="dataTypeChName" label="数据类型" width="100px" show-overflow-tooltip>
<template #default="scope">
<span>{{ scope.row["dataTypeChName"] || '--' }}</span>
</template>
</el-table-column>
<el-table-column label="值域" width="280px" align="left" fixed="right">
<template #default="scope">
<span v-if="scope.row.dataType == 'json' || scope.row.dataType == 'text' || !scope.row.dataType || scope.row.dataType == 'string'">--</span>
<template v-else>
<el-input v-show="scope.row.dataType == 'varchar' || scope.row.dataType == 'char' || scope.row.dataType == 'bit'" v-model.trim="scope.row.dataRange" clearable placeholder="多值按照分号;分隔"> </el-input>
<div class="range-sum" v-show="scope.row.dataType == 'datetime' || scope.row.dataType == 'date'">
<el-date-picker
v-model="scope.row.dateValueRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
:unlink-panels="false"
:disabled="props.readonly"
/>
</div>
<div class="range-sum" v-show="scope.row.dataType == 'int' || scope.row.dataType == 'decimal' || scope.row.dataType == 'tinyint' || scope.row.dataType == 'timestamp' || scope.row.dataType == 'time'">
<el-input :disabled="props.readonly" v-model.trim="scope.row.startValue" placeholder="请输入" clearable> </el-input>
<span class="text"></span>
<el-input :disabled="props.readonly" v-model.trim="scope.row.endValue" placeholder="请输入" clearable> </el-input>
</div>
</template>
</template>
</el-table-column>
</el-table>
</div>
</div>
<template #footer v-if="!props.readonly">
<div class="dialog-footer">
<el-button @click="cancelValueRangeDialog" v-preReClick>取消</el-button>
<el-button @click="submitValueRange" type="primary" v-preReClick>确定</el-button>
</div>
</template>
</el-dialog>
<!-- 表行数检查 -->
<el-dialog v-model="tableRowDialogVisible" title="规则设置" width="750" :modal="true" :close-on-click-modal="false"
......@@ -1655,7 +1888,7 @@ defineExpose({
padding-left: 8px;
}
.el-input {
.filter-edit .el-input {
margin-left: 8px;
width: calc(100% - 16px);
}
......@@ -1679,6 +1912,36 @@ defineExpose({
}
}
// 值域范围检查右边的字段
.table-field-right {
width: calc(100% - 200px);
.left-title {
border-left: 1px solid #d9d9d9;
}
:deep(.el-table) {
td.el-table__cell {
padding: 2px 0;
height: 36px;
}
}
.range-sum {
width: 100%;
display: inline-flex;
align-items: center;
.text {
margin: 0px 4px;
}
:deep(.el-input) {
width: 50%;
}
}
}
.empty-table-field {
.left-title {
border-left: 1px solid #d9d9d9;
......
......@@ -304,6 +304,34 @@ const transformRulesInfo = (info: any) => {
})]
}));
})
} else if (info.ruleCode == 'value_of_range') {
let subjectTables = toSubjectTables.value;
for (const ds in info.ruleFields) {
let fields = info.ruleFields[ds];
let tableInfo = subjectTables.find(t => t.enName === ds);
modelRules.push(Object.assign({}, {
modelGroupGuid: modelGroupGuid.value,
name: tableInfo.chName,
subjectName: tableInfo.enName,
subjectGuid: tableInfo.guid,
dataSourceGuid: tableInfo.dataSourceGuid,
databaseName: tableInfo.dataServerName,
modelRuleConfList: [Object.assign({}, info, {
ruleField: fields.map(f => {
return {
guid: f.guid,
enName: f.enName,
chName: f.chName,
dataType: f.dataType,
startValue: f.startValue,
endValue: f.endValue,
dataRange: f.dataRange
}
}),
ruleFields: ''
})]
}));
}
}
return modelRules;
}
......
......@@ -120,6 +120,26 @@ const transformRulesInfo = (info: any) => {
differenceRange: info.rows[0].differenceRange,
rows: ''
});
} else if (info.ruleCode == 'value_of_range') {
let subjectName = detailInfo.value.subjectName;
let fields = info.ruleFields[subjectName];
return Object.assign({}, info, {
guid: ruleGuid,
qualityModelGuid: detailInfo.value.qualityModelGuid,
ruleCode: detailInfo.value.ruleCode,
ruleField: fields.map(f => {
return {
guid: f.guid,
enName: f.enName,
chName: f.chName,
dataType: f.dataType,
startValue: f.startValue,
endValue: f.endValue,
dataRange: f.dataRange
}
}),
ruleFields: ''
});
}
}
......
......@@ -116,6 +116,25 @@ const transformRulesInfo = (info) => {
differenceRange: info.rows[0].differenceRange,
rows: ''
});
} else if (info.ruleCode === 'value_of_range') {
let subjectName = modelDetailInfo.value.subjectName;
let fields = info.ruleFields[subjectName];
return Object.assign({}, info, {
qualityModelGuid: modelGuid,
ruleCode: info.ruleCode,
ruleField: fields.map(f => {
return {
guid: f.guid,
enName: f.enName,
chName: f.chName,
dataType: f.dataType,
startValue: f.startValue,
endValue: f.endValue,
dataRange: f.dataRange
}
}),
ruleFields: ''
});
}
}
......
......@@ -17,8 +17,7 @@ import {
deleteImportData,
getImportData,
exportDictionary,
getStandardSetTree,
getDictionaryTree,
exportCollectTask,
getImageContent
} from '@/api/modules/queryService';
import { commonPageConfig } from '@/utils/enum';
......@@ -347,6 +346,14 @@ const exportData = (ids: any = null) => {
exportDictionary(params).then((res: any) => {
download(res, '数据字典模板.xlsx', 'excel')
});
} else if (tabsActiveName.value == 'importFile' && isfileImport == '4') {
exportCollectTask({
importTypes: [
"0042"
]
}).then((res: any) => {
download(res, '元数据模板.xlsx', 'excel')
});
}
}
......@@ -360,13 +367,28 @@ const importData = (info) => {
// dialogInfo.value.footer.btns.map((item: any) => delete item.disabled)
return
}
let paramUrl = '';
if (isfileImport == '2') {
uploadFiles.value.forEach((item: any, index: number) => {
params.append("file", item.raw);
});
let paramUrl = '';
if (isfileImport == '2') {
paramUrl = `${import.meta.env.VITE_API_ASSET_BASEURL}/dam-catalog-table/excel-by-subject-guid?staffGuid=${userData.staffGuid}&subjectGuid=${route.query.bizGuid}`
} else if (isfileImport == '4') {
if (!info.collectTaskName) {
ElMessage({
type: 'error',
message: '请填写采集任务名称'
})
return;
}
uploadFiles.value.forEach((item: any, index: number) => {
params.append("uploadFile", item.raw);
});
paramUrl = `${import.meta.env.VITE_APP_PLAN_BASEURL}/meta-collect-task/meta-collect-import?staffGuid=${userData.staffGuid}&collectTaskName=${info.collectTaskName}`
} else {
uploadFiles.value.forEach((item: any, index: number) => {
params.append("file", item.raw);
});
paramUrl = `${import.meta.env.VITE_APP_ADD_FILE}/import-data/import-common?importType=${importType.value}&staffGuid=${userData.staffGuid}&tenantGuid=${userData.tenantGuid}&dataType=2`
if (info && Object.keys(info).length) {
paramUrl += `&extendFields=${JSON.stringify(info)}`
......@@ -424,6 +446,8 @@ const setUploadInfo = () => {
} else {
if (isfileImport == '2') {
importType.value = '0034';
} else if (isfileImport == '4') {
importType.value = '0042';
} else {
importType.value = '0033';
}
......
Styling with Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!