Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions ui/src/components/COMPONENT_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,12 @@ Dialog、Drawer、Popover、嵌套区域等其他大、小表格均禁止开启
多选“全部”只操作可用选项,保留已选禁用项;无草稿选择时仍通过“重置”清空。
过长的扁平选项文案省略,悬停查看完整文案。

多选模式可通过 `filterable` 开启选项搜索,默认关闭,其他模式不受影响。搜索按 `label` 在前端
进行不区分大小写的包含匹配,忽略关键词首尾空格,不请求接口、不提交筛选条件。
搜索框固定在滚动区上方,每次打开清空关键词并回填已生效的勾选;无匹配时展示空状态。
搜索开启后,“全部”仅操作当前匹配的可用选项,保留隐藏的勾选项和已选禁用项;
确定提交完整草稿,重置清空所有勾选,页面的后台查询参数保持原有约定。

级联默认配置为 `{ multiple: true, checkStrictly: true, emitPath: false, showPrefix: false }`,与 v2 文档标签筛选一致:
父子节点独立选择,隐藏节点前的选择框。静态多选时,组件保留面板的原始勾选,
对外将选中的父组递归展开为可用叶子节点并去重;页面直接使用回写值,不再自行转换父组。
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,37 @@ const props = withDefaults(
modelValue: CheckboxValueType[]
options: TableFilterOption[]
width?: string | number
filterable?: boolean
}>(),
{ width: 192 },
{ width: 192, filterable: false },
)
const emit = defineEmits<{ select: [value: CheckboxValueType[]]; open: [] }>()
defineSlots<{ default(): unknown }>()

// 打开时回填草稿,确认或重置后提交。
const visible = ref(false)
const pendingValues = ref<CheckboxValueType[]>([])
const allOptionValues = computed(() => props.options.filter((option) => !option.disabled).map(({ value }) => value))
const searchKeyword = ref('')
const filteredOptions = computed(() => {
const keyword = searchKeyword.value.trim().toLowerCase()
return props.filterable && keyword ? props.options.filter((option) => option.label.toLowerCase().includes(keyword)) : props.options
})
const allOptionValues = computed(() => filteredOptions.value.filter((option) => !option.disabled).map(({ value }) => value))
const allSelected = computed(() => allOptionValues.value.length > 0 && allOptionValues.value.every((value) => pendingValues.value.includes(value)))
const selectionIndeterminate = computed(() => allOptionValues.value.some((value) => pendingValues.value.includes(value)) && !allSelected.value)

function handleOpen() {
searchKeyword.value = ''
pendingValues.value = [...props.modelValue]
emit('open')
}

function handleSelectAllChange(checked: CheckboxValueType) {
const disabledValues = pendingValues.value.filter((value) => props.options.some((option) => option.value === value && option.disabled))
pendingValues.value = checked ? [...disabledValues, ...allOptionValues.value] : disabledValues
// 搜索时保留隐藏的勾选项;未开启搜索时沿用原有全选规则。
const retainedValues = pendingValues.value.filter((value) =>
props.filterable ? !allOptionValues.value.includes(value) : props.options.some((option) => option.value === value && option.disabled),
)
pendingValues.value = checked ? [...new Set([...retainedValues, ...allOptionValues.value])] : retainedValues
}

function handleReset() {
Expand All @@ -47,16 +57,31 @@ function handleConfirm() {
<template>
<el-popover v-model:visible="visible" placement="bottom-start" trigger="click" :width="width" @before-enter="handleOpen">
<template #reference><slot /></template>
<div v-if="filterable" class="px-3 pt-3">
<MkSearchInput v-model="searchKeyword" />
</div>
<el-scrollbar max-height="280px">
<div class="flex-column gap-2 p-3 pb-0">
<el-checkbox :indeterminate="selectionIndeterminate" :model-value="allSelected" class="mr-0!" @change="handleSelectAllChange">
<el-checkbox
:indeterminate="selectionIndeterminate"
:model-value="allSelected"
:disabled="filterable && !allOptionValues.length"
class="mr-0!"
@change="handleSelectAllChange"
>
全部
</el-checkbox>
<el-checkbox-group v-model="pendingValues" class="mk-checkbox-group-vertical gap-2">
<el-checkbox v-for="option in options" :key="`${typeof option.value}:${option.value}`" :value="option.value" :disabled="option.disabled">
<el-checkbox
v-for="option in filteredOptions"
:key="`${typeof option.value}:${option.value}`"
:value="option.value"
:disabled="option.disabled"
>
<span class="block min-w-0 truncate" :title="option.label">{{ option.label }}</span>
</el-checkbox>
</el-checkbox-group>
<MkEmpty v-if="filterable && !filteredOptions.length" type="search" description="暂无匹配结果" :image-size="64" />
</div>
</el-scrollbar>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issues Found

1. Semantics of "Select All" under search are ambiguous / possibly buggy

In handleSelectAllChange, when filterable is true, the retained values are computed as:

pendingValues.value.filter((value) => !allOptionValues.value.includes(value))

This works for retaining hidden selections — but there's a subtle issue: when checked is false, this keeps all values not in the filtered set, which is correct. However, the "全部" checkbox itself toggles based on filtered options only. That behavior is fine semantically, but should be documented — users may expect "select all" to mean "all options", not "all visible options". Consider renaming or adding a tooltip.

2. Indeterminate state ignores hidden selections

selectionIndeterminate and allSelected are both computed from allOptionValues (filtered). If a user searches, selects some items, then changes the search, the state of "全部" reflects only the current filter. This is likely intended but can confuse users — make sure it's documented.

3. disabled checkbox in "全部" uses inconsistent logic

:disabled="filterable && !allOptionValues.length"

When filterable is false, this is never disabled — even if there are no options at all. The original code had no disable guard. Consider making it consistent: :disabled="!allOptionValues.length" regardless of filterable.

4. Search input placement renders outside the scrollbar → layout jump

The MkSearchInput is placed above el-scrollbar, so it won't scroll with content. That's usually desirable, but make sure max-height="280px" on the scrollbar doesn't cause the popover to grow oddly. Also, the empty-state (MkEmpty) lives inside the scroll area while the checkbox for "全部" stays visible — verify that looks acceptable when there are 0 filtered results.

5. Missing whitespace-trimmed keyword state reset timing

searchKeyword.value = '' is reset in handleOpen, which fires on @before-enter. This means during the close animation the previous keyword is still shown briefly, which is fine. But if a user reopens quickly, before-enter may not fire. Prefer explicitly resetting in a watcher on visible:

watch(visible, (v) => { if (v) searchKeyword.value = '' })

6. Set spread loses order = OK but could surprise

[...new Set([...retainedValues, ...allOptionValues.value])] preserves insertion order — good. But note that retainedValues comes first, so the resulting order differs from props.options ordering. If consumers depend on order, sort by props.options index instead. Minor.

7. Case sensitivity only lowercases label, not non-string labels

option.label.toLowerCase()

Assumes label is always a string. If TableFilterOption.label can be number/VNode, this will throw. Guard it or type it as string.

8. Performance: filtering + includes inside every/some is O(n·m)

For large option lists this is O(n²) per computed run. Consider using a Set of pendingValues inside those computeds:

const pendingSet = computed(() => new Set(pendingValues.value))

Then use .has(value).

9. Duplicated key generation

key={${typeof option.value}:${option.value}} is repeated in two places — extract to a helper function for readability.

10. No debounce on search

If options is very large, filtering on each keystroke and re-rendering thousands of checkboxes will lag. Consider debouncing searchKeyword (e.g., 150–200ms) or virtualizing the list.

11. Accessibility

  • MkSearchInput should have an aria-label (e.g., "Search options").
  • MkEmpty inside a role group is fine, but ensure the checkbox group remains correctly labeled after the empty state appears.

Summary of recommended fixes (priority order)

  1. Guard option.label typing for .toLowerCase().
  2. Use a Set for pendingValues lookups in computeds.
  3. Reset searchKeyword via a watch on visible rather than before-enter.
  4. Unify the disabled logic on the "全部" checkbox.
  5. Add debounce to search input.
  6. Add aria-label to search.

Expand Down
4 changes: 3 additions & 1 deletion ui/src/components/global/mk-table/mk-table-filter/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ const props = withDefaults(
resetValue?: T
cascaderProps?: CascaderProps
width?: string | number
filterable?: boolean
}>(),
{ mode: 'multiple', options: () => [] },
{ mode: 'multiple', options: () => [], filterable: false },
)
defineSlots<{ default(props: { value: T }): unknown }>()
const emit = defineEmits<{ change: [value: T]; open: [] }>()
Expand Down Expand Up @@ -67,6 +68,7 @@ const filterProps = computed(() => {
modelValue: selectedValue.value as TableFilterOption['value'][],
options: props.options as TableFilterOption[],
width: props.width,
filterable: props.filterable,
}
})

Expand Down
1 change: 1 addition & 0 deletions ui/src/views/system/operate-logs/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ onMounted(() => {
mode="multiple"
v-model="selectedWorkspaceIds"
label="工作空间"
filterable
:options="workspaceOptions"
@change="handleWorkspaceChange"
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,14 @@ onMounted(() => {
</el-table-column>
<el-table-column v-if="auth.isEE" prop="workspace_name" min-width="160" show-overflow-tooltip>
<template #header>
<MkTableFilter mode="multiple" v-model="selectedWorkspaceIds" label="工作空间" :options="workspaceOptions" @change="handleFilterChange" />
<MkTableFilter
mode="multiple"
v-model="selectedWorkspaceIds"
label="工作空间"
filterable
:options="workspaceOptions"
@change="handleFilterChange"
/>
</template>
</el-table-column>
<el-table-column prop="nick_name" label="创建者" min-width="120" show-overflow-tooltip />

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary of the change
The only functional change is adding filterable to <MkTableFilter>, which presumably enables a search/filter box inside the dropdown. That's harmless as long as MkTableFilter accepts and correctly handles that prop.

Potential issues / things to verify

  1. Prop support & semantics — Confirm MkTableFilter actually declares filterable in its props and implements filtering (e.g., filters options by label/value). If it doesn't, this is a no-op or a silently dropped attribute.

  2. Filtering options must be reactive — If workspaceOptions is large or loaded async, make sure the internal filter recomputes when options changes (computed/watch rather than caching).

  3. Localization / matching behavior — With Chinese labels ("工作空间"), ensure the filter does case-insensitive / substring matching appropriately; consider whether pinyin or punctuation normalization is expected by users.

  4. Large option lists — If workspaceOptions can be hundreds/thousands of entries, filtering on every keystroke may cause jank. Consider debouncing input or using virtual scrolling (el-select-vue / el-select with filterable + multiple supports native filtering but with many items it can lag).

  5. Selected values vs filtered results — In multiple-selection mode, filtering hides selected tags less obviously; verify selected items not matching the query are still retained in selectedWorkspaceIds and displayed correctly.

  6. @change timing — Filtering is UI-only; handleFilterChange should fire on selection change, not on filter input. Confirm the component doesn't emit change while typing.

  7. Reset behavior — After closing/reopening the dropdown, does the filter text reset? Usually desirable; otherwise stale queries confuse users.

  8. No-results state — Ensure an empty-state message is shown when the filter matches nothing.

  9. Accessibility — Check keyboard navigation still works with the filter input (arrow keys, Esc to clear/close).

Minor style/nit

  • Consider keeping the original one-line form if the project's linter/prettier has a low print-width; the multi-line version is fine and arguably more readable.
  • Prop order is consistent with existing usage elsewhere — good.
  • The diff hunk header @@ -156,7 +156,14 @@ indicates 7 lines replaced by 14 (one line split into a block); looks correct given the change.

Overall: No irregularities in syntax. The main risk is whether filterable is supported and behaves well with large/asynchronous option sets — worth a quick test with many workspaces and verifying selection persistence during filtering.

Expand Down
9 changes: 8 additions & 1 deletion ui/src/views/system/resource-management/knowledge/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,14 @@ onMounted(() => {
</el-table-column>
<el-table-column v-if="auth.isEE" prop="workspace_name" min-width="160" show-overflow-tooltip>
<template #header>
<MkTableFilter mode="multiple" v-model="selectedWorkspaceIds" label="工作空间" :options="workspaceOptions" @change="handleFilterChange" />
<MkTableFilter
mode="multiple"
v-model="selectedWorkspaceIds"
label="工作空间"
filterable
:options="workspaceOptions"
@change="handleFilterChange"
/>
</template>
</el-table-column>
<el-table-column prop="nick_name" label="创建者" min-width="120" show-overflow-tooltip />

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change itself is minor (just adding a filterable prop and reformatting), but here are some observations:

Potential issues

  1. Missing key on the column – Not critical since it's static, but if columns toggle via v-if, ensure reactivity is fine.

  2. filterable without client-side fallback – Verify that MkTableFilter handles large workspaceOptions lists well when filterable is enabled. If the list is huge, consider server-side search instead of local filtering.

  3. v-model + @change duplication – You're using both two-way binding (v-model="selectedWorkspaceIds") and @change="handleFilterChange". Make sure handleFilterChange doesn't rely on stale values; with v-model the update order matters (the change event may fire before the model updates in some implementations).

  4. auth.isEE gating – Good to confirm that workspaceOptions is populated before this component renders, otherwise filterable will just show an empty dropdown.

Optimization suggestions

  1. Consistent option labeling – For filterable UX, make sure each option has a clear, unique label string so the user can match by typing.

  2. Add debounce for filtering – If MkTableFilter filters locally and the options array is large (>200), debounce the input or use virtual scrolling.

  3. Preserve filter state – When switching pages or re-mounting (e.g., route change), consider persisting selectedWorkspaceIds if this is expected behavior.

  4. Extract repeated props – If filterable becomes common across multiple MkTableFilter usages, consider defaulting it inside MkTableFilter rather than repeating it.

Minor style

  • The multi-line formatting is fine for readability, though for just one added attribute you could keep it inline.
  • No functional irregularities detected in this hunk — the diff is low-risk.

Expand Down
9 changes: 8 additions & 1 deletion ui/src/views/system/resource-management/model/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,14 @@ onMounted(() => {
<el-table-column prop="model_name" label="基础模型" min-width="180" show-overflow-tooltip />
<el-table-column v-if="auth.isEE" prop="workspace_name" min-width="160" show-overflow-tooltip>
<template #header>
<MkTableFilter mode="multiple" v-model="selectedWorkspaceIds" label="工作空间" :options="workspaceOptions" @change="handleFilterChange" />
<MkTableFilter
mode="multiple"
v-model="selectedWorkspaceIds"
label="工作空间"
filterable
:options="workspaceOptions"
@change="handleFilterChange"
/>
</template>
</el-table-column>
<el-table-column prop="nick_name" label="创建者" min-width="120" show-overflow-tooltip />
Expand Down
9 changes: 8 additions & 1 deletion ui/src/views/system/resource-management/tool/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,14 @@ onMounted(() => {
</el-table-column>
<el-table-column v-if="auth.isEE" prop="workspace_name" min-width="160" show-overflow-tooltip>
<template #header>
<MkTableFilter mode="multiple" v-model="selectedWorkspaceIds" label="工作空间" :options="workspaceOptions" @change="handleFilterChange" />
<MkTableFilter
mode="multiple"
v-model="selectedWorkspaceIds"
label="工作空间"
filterable
:options="workspaceOptions"
@change="handleFilterChange"
/>
</template>
</el-table-column>
<el-table-column prop="nick_name" label="创建者" min-width="120" show-overflow-tooltip />
Expand Down
Loading