feat(测试用例): 测试用例支持高级搜索&修改高级搜索交互

This commit is contained in:
teukkk 2024-09-04 19:05:49 +08:00 committed by Craftsman
parent 73a011985c
commit 543909c358
16 changed files with 259 additions and 82 deletions

View File

@ -245,7 +245,7 @@ export function getDefaultLocale() {
// 视图列表
export function getViewList(viewType: string, scopeId: string) {
return MSR.get<ViewList>({ url: `/user-view/${viewType}/grouped/list`, params: scopeId });
return MSR.get<ViewList>({ url: `/user-view/${viewType}/grouped/list`, params: { scopeId } });
}
// 视图详情
export function getViewDetail(viewType: string, id: string) {
@ -261,5 +261,5 @@ export function addView(viewType: string, data: ViewParams) {
}
// 删除视图
export function deleteView(viewType: string, id: string) {
return MSR.post({ url: `/user-view/${viewType}/delete/${id}` });
return MSR.get({ url: `/user-view/${viewType}/delete/${id}` });
}

View File

@ -302,7 +302,8 @@
.arco-input-tag-disabled,
.arco-input-disabled,
.arco-textarea-disabled,
.arco-select-view-disabled
.arco-select-view-disabled,
.arco-picker-disabled
):hover {
border-color: rgb(var(--primary-5)) !important;
background-color: white;
@ -314,7 +315,8 @@
.arco-input-tag-disabled,
.arco-select-view-disabled,
.arco-textarea-disabled,
.arco-input-disabled {
.arco-input-disabled,
.arco-picker-disabled {
border-color: var(--color-text-n9) !important;
background-color: var(--color-text-n9) !important;
}

View File

@ -4,7 +4,7 @@
class="hidden-item"
hide-asterisk
field="name"
:validate-trigger="['blur', 'input']"
:validate-trigger="['change', 'input']"
:rules="[{ required: true, message: t('advanceFilter.viewNameRequired') }, { validator: validateName }]"
>
<a-input
@ -60,7 +60,16 @@
});
}
function validateForm(cb: () => void) {
formRef.value?.validate(async (errors) => {
if (!errors) {
cb();
}
});
}
defineExpose({
inputFocus,
validateForm,
});
</script>

View File

@ -2,17 +2,18 @@
<MsDrawer v-model:visible="visible" :mask="false" :width="600">
<template #title>
<ViewNameInput
v-show="isShowNameInput"
v-if="isShowNameInput"
ref="viewNameInputRef"
v-model:form="formModel"
:all-names="allViewNames"
:all-names="allViewNames.filter((name) => name !== savedFormModel.name)"
@handle-submit="isShowNameInput = false"
/>
<div v-show="!isShowNameInput" class="flex flex-1 items-center gap-[8px] overflow-hidden">
<div v-else class="flex flex-1 items-center gap-[8px] overflow-hidden">
<a-tooltip :content="formModel.name">
<div class="one-line-text"> {{ formModel.name }}</div>
</a-tooltip>
<MsIcon
v-if="formModel?.internalViewKey !== 'ALL_DATA'"
type="icon-icon_edit_outlined"
class="min-w-[16px] cursor-pointer hover:text-[rgb(var(--primary-5))]"
@click="showNameInput"
@ -92,6 +93,18 @@
:max-length="255"
:placeholder="t('common.pleaseInput')"
/>
<MsSelect
v-else-if="item.type === FilterType.MEMBER"
v-model:model-value="item.value"
allow-clear
allow-search
:placeholder="t('common.pleaseSelect')"
:disabled="isValueDisabled(item)"
:options="props.memberOptions"
multiple
:search-keys="['label']"
:max-tag-count="1"
/>
<MsSelect
v-else-if="item.type === FilterType.SELECT"
v-model:model-value="item.value"
@ -180,12 +193,13 @@
{{ t('advanceFilter.addCondition') }}
</MsButton>
<template #footer>
<div v-show="!isSaveAsView" class="flex items-center gap-[8px]">
<div v-if="!isSaveAsView" class="flex items-center gap-[8px]">
<a-button type="primary" @click="handleFilter">{{ t('common.filter') }}</a-button>
<a-button class="mr-[16px]" @click="handleReset">{{ t('common.reset') }}</a-button>
<MsButton
v-if="formModel?.internalViewKey !== 'ALL_DATA'"
type="text"
:loading="saveLoading"
class="!text-[var(--color-text-1)]"
@click="handleSaveView"
>
@ -195,14 +209,14 @@
{{ t('advanceFilter.saveAsView') }}
</MsButton>
</div>
<div v-show="isSaveAsView" class="flex items-center gap-[8px]">
<div v-else class="flex items-center gap-[8px]">
<ViewNameInput
ref="saveAsViewNameInputRef"
v-model:form="saveAsViewForm"
class="w-[240px]"
:all-names="allViewNames"
/>
<a-button type="primary" @click="handleAddView">{{ t('common.save') }}</a-button>
<a-button type="primary" :loading="addLoading" @click="handleAddView">{{ t('common.save') }}</a-button>
<a-button @click="handleCancelSaveAsView">{{ t('common.cancel') }}</a-button>
</div>
</template>
@ -221,6 +235,7 @@
import { addView, getViewDetail, updateView } from '@/api/modules/user/index';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { SelectValue } from '@/models/projectManagement/menuManagement';
import { FilterType, OperatorEnum, ViewTypeEnum } from '@/enums/advancedFilterEnum';
@ -234,6 +249,8 @@
viewType: ViewTypeEnum;
currentView: string; //
allViewNames: string[];
canNotAddView: boolean;
memberOptions: { label: string; value: string }[];
}>();
const emit = defineEmits<{
(e: 'handleFilter', value: FilterResult): void;
@ -242,6 +259,7 @@
const visible = defineModel<boolean>('visible', { required: true });
const { t } = useI18n();
const appStore = useAppStore();
const defaultFormModel: FilterForm = {
name: '',
@ -361,6 +379,7 @@
//
function handleReset() {
formModel.value = cloneDeep(savedFormModel.value);
isShowNameInput.value = false;
}
//
function handleFilter() {
@ -378,29 +397,49 @@
handleFilter();
}
);
watch(
() => visible.value,
async (val) => {
//
if (!val && formModel.value?.id !== props.currentView) {
await getUserViewDetail(props.currentView);
}
}
);
const isSaveAsView = ref(false);
const saveAsViewForm = ref({ name: '' });
const saveAsViewNameInputRef = ref<InstanceType<typeof ViewNameInput>>();
//
function resetToNewViewForm() {
// TODO lmy
//
let name = '';
for (let i = 1; i <= 10; i++) {
const defaultName = `${t('advanceFilter.unnamedView')}${String(i).padStart(3, '0')}`;
if (!props.allViewNames.includes(defaultName)) {
name = defaultName;
break;
}
}
formModel.value = {
...cloneDeep(defaultFormModel),
name: '未命名视图001',
name,
};
savedFormModel.value = cloneDeep(formModel.value);
}
//
function handleSaveView() {
// TODO lmy
const saveLoading = ref(false);
function realSaveView() {
formRef.value?.validate(async (errors) => {
if (!errors) {
try {
saveLoading.value = true;
if (formModel.value.id) {
await updateView(props.viewType, { ...getParams(), name: formModel.value.name, id: formModel.value.id });
} else {
await addView(props.viewType, { ...getParams(), name: formModel.value.name, id: formModel.value.id });
await addView(props.viewType, {
...getParams(),
scopeId: appStore.currentProjectId,
name: formModel.value.name,
id: formModel.value.id,
});
}
Message.success(t('common.saveSuccess'));
savedFormModel.value = cloneDeep(formModel.value);
@ -408,15 +447,34 @@
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
saveLoading.value = false;
}
}
});
}
function handleSaveView() {
if (viewNameInputRef.value) {
viewNameInputRef.value?.validateForm(realSaveView);
} else {
realSaveView();
}
}
//
const isSaveAsView = ref(false);
const saveAsViewForm = ref({ name: '' });
const saveAsViewNameInputRef = ref<InstanceType<typeof ViewNameInput>>();
function handleToSaveAs() {
if (props.canNotAddView) {
Message.warning(t('advanceFilter.maxViewTip'));
return;
}
formRef.value?.validate((errors) => {
if (!errors) {
isSaveAsView.value = true;
nextTick(() => {
saveAsViewNameInputRef.value?.inputFocus();
});
}
});
}
@ -426,17 +484,29 @@
saveAsViewForm.value.name = '';
}
//
async function handleAddView() {
// TODO lmy saveAsViewNameInputRef
const addLoading = ref(false);
async function realAddView() {
try {
await addView(props.viewType, { ...getParams(), name: formModel.value.name, id: formModel.value.id });
addLoading.value = true;
await addView(props.viewType, {
...getParams(),
scopeId: appStore.currentProjectId,
name: saveAsViewForm.value.name,
id: formModel.value.id,
});
Message.success(t('common.saveSuccess'));
emit('refreshViewList');
handleCancelSaveAsView();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
addLoading.value = false;
}
}
async function handleAddView() {
saveAsViewNameInputRef.value?.validateForm(realAddView);
}
defineExpose({
resetToNewViewForm,

View File

@ -38,6 +38,7 @@ export const operatorOptionsMap: Record<string, { value: string; label: string }
[FilterType.RADIO]: COMMON_SELECTION_OPERATORS,
[FilterType.CHECKBOX]: COMMON_SELECTION_OPERATORS,
[FilterType.SELECT]: COMMON_SELECTION_OPERATORS,
[FilterType.MEMBER]: COMMON_SELECTION_OPERATORS,
[FilterType.TAGS_INPUT]: [EMPTY, CONTAINS, NO_CONTAINS, COUNT_LT, COUNT_GT],
[FilterType.TREE_SELECT]: [BELONG_TO, NOT_BELONG_TO],
[FilterType.DATE_PICKER]: [BETWEEN, EQUAL, EMPTY, NOT_EMPTY],

View File

@ -34,8 +34,9 @@
<a-select
v-if="props.viewType"
v-model:model-value="currentView"
:loading="viewListLoading"
:trigger-props="{ contentClass: 'view-select-trigger' }"
class="w-[160px]"
class="w-[180px]"
show-footer-on-empty
>
<template #prefix> {{ t('advanceFilter.view') }} </template>
@ -45,25 +46,34 @@
</a-option>
</a-optgroup>
<a-optgroup :label="t('advanceFilter.myView')">
<a-option v-for="item in customViews" :key="item.id" :value="item.id">
{{ item.name }}
<div class="flex">
<a-tooltip :content="t('common.rename')">
<MsButton type="text" status="secondary" class="!mr-[4px]" @click.stop="handleRenameView(item)">
<MsIcon type="icon-icon_edit_outlined" class="hover:text-[rgb(var(--primary-4))]" size="12" />
</MsButton>
</a-tooltip>
<a-tooltip :content="t('advanceFilter.deleteView')">
<MsButton type="text" status="secondary" @click.stop="handleDeleteView(item)">
<MsIcon
type="icon-icon_delete-trash_outlined1"
class="hover:text-[rgb(var(--primary-4))]"
size="12"
/>
</MsButton>
</a-tooltip>
</div>
</a-option>
<template v-for="item in customViews" :key="item.id">
<a-option v-show="!item.isShowNameInput" :value="item.id">
<div>{{ item.name }}</div>
<div class="select-extra flex">
<a-tooltip :content="t('common.rename')">
<MsButton type="text" status="secondary" class="!mr-[4px]" @click="handleToRenameView(item)">
<MsIcon type="icon-icon_edit_outlined" class="hover:text-[rgb(var(--primary-4))]" size="12" />
</MsButton>
</a-tooltip>
<a-tooltip :content="t('advanceFilter.deleteView')">
<MsButton type="text" :disabled="deleteLoading" status="secondary" @click="handleDeleteView(item)">
<MsIcon
type="icon-icon_delete-trash_outlined1"
class="hover:text-[rgb(var(--primary-4))]"
size="12"
/>
</MsButton>
</a-tooltip>
</div>
</a-option>
<ViewNameInput
v-if="item.isShowNameInput"
:ref="(el:refItem) => setNameInputRefMap(el, item)"
v-model:form="formModel"
:all-names="allViewNames.filter((name) => name !== item.name)"
@handle-submit="handleRenameView"
/>
</template>
</a-optgroup>
<template #footer>
<div class="flex cursor-pointer items-center gap-[8px]" @click="toNewView">
@ -111,6 +121,8 @@
:all-view-names="allViewNames"
:config-list="props.filterConfigList"
:custom-list="props.customFieldsConfigList"
:can-not-add-view="canNotAddView"
:member-options="memberOptions"
@handle-filter="handleFilter"
@refresh-view-list="getUserViewList"
/>
@ -122,9 +134,11 @@
import MsButton from '@/components/pure/ms-button/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsTag from '../ms-tag/ms-tag.vue';
import ViewNameInput from './components/viewNameInput.vue';
import FilterDrawer from './filterDrawer.vue';
import { deleteView, getViewList } from '@/api/modules/user/index';
import { getProjectOptions } from '@/api/modules/project-management/projectMember';
import { deleteView, getViewList, updateView } from '@/api/modules/user/index';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
@ -159,42 +173,94 @@
const currentView = ref(''); //
const internalViews = ref<ViewItem[]>([]);
const customViews = ref<ViewItem[]>([]);
const viewListLoading = ref(false);
const allViewNames = computed(() => [...internalViews.value, ...customViews.value].map((item) => item.name));
const canNotAddView = computed(() => customViews.value.length >= 10);
async function getUserViewList() {
try {
viewListLoading.value = true;
const res = await getViewList(props.viewType as ViewTypeEnum, appStore.currentProjectId);
internalViews.value = res.internalViews;
customViews.value = res.customViews;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
viewListLoading.value = false;
}
}
const memberOptions = ref<{ label: string; value: string }[]>([]);
async function getMemberOptions() {
const res = await getProjectOptions(appStore.currentProjectId);
memberOptions.value = [{ name: t('common.currentUser'), id: 'CURRENT_USER' }, ...res].map((e: any) => ({
label: e.name,
value: e.id,
}));
}
onMounted(async () => {
await getUserViewList();
currentView.value = internalViews.value[0].id;
if (props.viewType) {
getMemberOptions();
await getUserViewList();
currentView.value = internalViews.value[0]?.id;
}
});
const filterDrawerRef = ref<InstanceType<typeof FilterDrawer>>();
function toNewView() {
if (canNotAddView.value) {
Message.warning(t('advanceFilter.maxViewTip'));
return;
}
visible.value = true;
filterDrawerRef.value?.resetToNewViewForm();
}
function handleRenameView(item: ViewItem) {
// TODO lmy
type refItem = Element | ComponentPublicInstance | null;
const viewNameInputRefMap: Record<string, any> = {};
function setNameInputRefMap(el: refItem, item: ViewItem) {
if (el) {
viewNameInputRefMap[`${item.id}`] = el;
}
}
async function handleDeleteView(item: ViewItem) {
const formModel = ref({ name: '', id: '' });
function handleToRenameView(item: ViewItem) {
formModel.value.id = item.id;
formModel.value.name = item.name;
item.isShowNameInput = true;
nextTick(() => {
viewNameInputRefMap[item.id]?.inputFocus();
});
}
async function handleRenameView() {
try {
await deleteView(item.viewType, item.id);
Message.success(t('common.deleteSuccess'));
await updateView(props.viewType as string, { name: formModel.value.name, id: formModel.value.id });
Message.success(t('common.saveSuccess'));
getUserViewList();
currentView.value = internalViews.value[0].id;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
//
const deleteLoading = ref(false);
async function handleDeleteView(item: ViewItem) {
try {
deleteLoading.value = true;
await deleteView(props.viewType as string, item.id);
Message.success(t('common.deleteSuccess'));
await getUserViewList();
if (item.id === currentView.value) {
currentView.value = internalViews.value[0].id;
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
deleteLoading.value = false;
}
}
const isAdvancedSearchMode = ref(false);
const handleFilter = (filter: FilterResult) => {
keyword.value = '';
@ -242,6 +308,14 @@
.arco-select-option-content {
@apply flex w-full items-center justify-between;
}
.select-extra {
visibility: hidden;
}
.arco-select-option:hover {
.select-extra {
visibility: visible;
}
}
.arco-select-dropdown-list-wrapper {
max-height: 255px;
}

View File

@ -23,6 +23,7 @@ export default {
'advanceFilter.operator.length.le': 'Length less than or equal to',
'advanceFilter.view': 'View',
'advanceFilter.unnamedView': 'Unnamed View',
'advanceFilter.systemView': 'System view',
'advanceFilter.myView': 'My view',
'advanceFilter.newView': 'New view',
@ -40,4 +41,5 @@ export default {
'advanceFilter.conditionRequired': 'Query condition cannot be empty',
'advanceFilter.filterContentRequired': 'Filter content cannot be empty',
'advanceFilter.filterTip': 'Filter mode, module filtering can only be operated in the current filter',
'advanceFilter.maxViewTip': 'Up to 10 views can be added',
};

View File

@ -23,6 +23,7 @@ export default {
'advanceFilter.operator.length.le': '长度小于等于',
'advanceFilter.view': '视图',
'advanceFilter.unnamedView': '未命名视图',
'advanceFilter.systemView': '系统视图',
'advanceFilter.myView': '我的视图',
'advanceFilter.newView': '新建视图',
@ -40,4 +41,5 @@ export default {
'advanceFilter.conditionRequired': '查询条件不能为空',
'advanceFilter.filterContentRequired': '筛选内容不能为空',
'advanceFilter.filterTip': '筛选模式,模块过滤仅可在当前过滤器中操作',
'advanceFilter.maxViewTip': '最多可添加 10 个视图',
};

View File

@ -52,7 +52,7 @@ export interface ConditionsItem extends CombineItem {
export interface FilterResult {
// 匹配模式 所有/任一
searchMode: AccordBelowType;
searchMode?: AccordBelowType;
// 高级搜索
conditions?: ConditionsItem[];
combine?: any; // TODO lmy 此为防报错占位 所有高级筛选都完成后 删除这一行
@ -69,6 +69,7 @@ export interface ViewItem {
pos?: number; // 自定义排序
createTime?: number;
updateTime?: number;
isShowNameInput?: boolean;
}
export interface ViewList {
internalViews: ViewItem[];

View File

@ -75,6 +75,11 @@
background-color: var(--color-text-n8);
}
}
.ms-button-text {
@apply p-0;
color: rgb(var(--primary-5));
}
.ms-button--secondary {
color: var(--color-text-2);
&:not(.ms-button-text, .ms-button--disabled):hover {
@ -106,9 +111,4 @@
padding: 0 2px;
font-size: 12px;
}
.ms-button-text {
@apply p-0;
color: rgb(var(--primary-5));
}
</style>

View File

@ -18,6 +18,7 @@ export enum FilterType {
INPUT = 'Input',
NUMBER = 'Number',
SELECT = 'Select',
MEMBER = 'Member',
DATE_PICKER = 'DatePicker',
TAGS_INPUT = 'TagsInput',
TREE_SELECT = 'TreeSelect',

View File

@ -208,4 +208,5 @@ export default {
'common.cutSuccess': 'Cut successfully',
'common.copySuccessToClipboard': 'Copied to clipboard',
'common.casePriority': 'Case Priority',
'common.currentUser': 'Current user',
};

View File

@ -208,4 +208,5 @@ export default {
'common.cutSuccess': '剪切成功',
'common.copySuccessToClipboard': '已复制到剪切板',
'common.casePriority': '用例等级',
'common.currentUser': '当前用户',
};

View File

@ -412,7 +412,6 @@
} from '@/api/modules/case-management/featureCase';
import { getSocket } from '@/api/modules/project-management/commonScript';
import { getCaseRelatedInfo } from '@/api/modules/project-management/menuManagement';
import { getProjectOptions } from '@/api/modules/project-management/projectMember';
import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal';
import { useAppStore, useTableStore } from '@/store';
@ -774,7 +773,6 @@
],
};
const memberOptions = ref<{ label: string; value: string }[]>([]);
const filterConfigList = computed<FilterFormItem[]>(() => [
{
title: 'caseManagement.featureCase.tableColumnID',
@ -787,7 +785,7 @@
type: FilterType.INPUT,
},
{
title: 'caseManagement.featureCase.tableColumnModule',
title: 'common.belongModule',
dataIndex: 'moduleId',
type: FilterType.TREE_SELECT,
treeSelectData: caseTreeData.value,
@ -800,44 +798,59 @@
multiple: true,
treeCheckable: true,
treeCheckStrictly: true,
maxTagCount: 2,
maxTagCount: 1,
},
},
{
title: 'caseManagement.featureCase.tableColumnVersion',
dataIndex: 'versionId',
title: 'caseManagement.featureCase.tableColumnReviewResult',
dataIndex: 'reviewStatus',
type: FilterType.SELECT,
selectProps: {
multiple: true,
options: reviewResultOptions.value,
},
},
{
title: 'caseManagement.featureCase.tableColumnExecutionResult',
dataIndex: 'lastExecuteResult',
type: FilterType.SELECT,
selectProps: {
multiple: true,
options: executeResultOptions.value,
},
},
{
title: 'caseManagement.featureCase.associatedDemand',
dataIndex: 'demand',
type: FilterType.INPUT,
},
{
title: 'caseManagement.featureCase.tableColumnCreateUser',
dataIndex: 'createUserName',
type: FilterType.SELECT,
selectProps: {
mode: 'static',
options: memberOptions.value,
},
title: 'caseManagement.featureCase.relatedAttachments',
dataIndex: 'attachment',
type: FilterType.INPUT,
},
{
title: 'caseManagement.featureCase.tableColumnCreateTime',
title: 'common.creator',
dataIndex: 'createUser',
type: FilterType.MEMBER,
},
{
title: 'common.createTime',
dataIndex: 'createTime',
type: FilterType.DATE_PICKER,
},
{
title: 'caseManagement.featureCase.tableColumnUpdateUser',
dataIndex: 'updateUserName',
type: FilterType.SELECT,
selectProps: {
mode: 'static',
options: memberOptions.value,
},
title: 'common.updateUserName',
dataIndex: 'updateUser',
type: FilterType.MEMBER,
},
{
title: 'caseManagement.featureCase.tableColumnUpdateTime',
title: 'common.updateTime',
dataIndex: 'updateTime',
type: FilterType.DATE_PICKER,
},
{
title: 'caseManagement.featureCase.tableColumnTag',
title: 'common.tag',
dataIndex: 'tags',
type: FilterType.TAGS_INPUT,
},
@ -846,8 +859,6 @@
async function initFilter() {
const result = await getCustomFieldsTable(currentProjectId.value);
memberOptions.value = await getProjectOptions(appStore.currentProjectId, keyword.value);
memberOptions.value = memberOptions.value.map((e: any) => ({ label: e.name, value: e.id }));
//
searchCustomFields.value = result.map((item: any) => {
const FilterTypeKey: keyof typeof FilterType = CustomTypeMaps[item.type].type;

View File

@ -67,6 +67,7 @@ export default {
'caseManagement.featureCase.moveTo': 'Move to',
'caseManagement.featureCase.copyTo': 'Copy to',
'caseManagement.featureCase.associatedDemand': 'Associated demand',
'caseManagement.featureCase.relatedAttachments': 'Related attachments',
'caseManagement.featureCase.generatingDependencies': 'Generative dependency',
'caseManagement.featureCase.addToPublic': 'Add to public case',
'caseManagement.featureCase.updateCase': 'Update Case',

View File

@ -67,6 +67,7 @@ export default {
'caseManagement.featureCase.moveTo': '移动到',
'caseManagement.featureCase.copyTo': '复制到',
'caseManagement.featureCase.associatedDemand': '关联需求',
'caseManagement.featureCase.relatedAttachments': '关联附件',
'caseManagement.featureCase.generatingDependencies': '生成依赖关系',
'caseManagement.featureCase.addToPublic': '添加到公共用例库',
'caseManagement.featureCase.updateCase': '更新用例',