Skip to content

Conversation

kagol
Copy link
Member

@kagol kagol commented Apr 22, 2025

PR

PR Checklist

Please check if your PR fulfills the following requirements:

  • The commit message follows our Commit Message Guidelines
  • Tests for the changes have been added (for bug fixes / features)
  • Docs have been added / updated (for bug fixes / features)

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Documentation content changes
  • Other... Please describe:

What is the current behavior?

Issue Number: N/A

What is the new behavior?

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

Summary by CodeRabbit

  • New Features

    • Improved multi-select dropdown tree behavior, including more precise selection and deselection of tags and tree nodes.
    • Added support for strict checking in the tree component.
    • Enhanced data binding to better synchronize model values with UI components.
  • Bug Fixes

    • Enhanced synchronization between selected tags and checked tree nodes, especially when removing tags in multi-select mode.
  • Tests

    • Expanded and refined test cases for multi-select dropdown tree and dropdown selection behaviors.
    • Updated test selectors for improved accessibility and reliability.
  • Chores

    • Removed experimental version metadata from documentation and menu entries.

Copy link

coderabbitai bot commented Apr 22, 2025

Walkthrough

This set of changes updates the tree-select component's data handling, selection logic, and its integration with the base select component. The internal state management is refactored to use a new modelValue array, and selection synchronization is improved through new utility and watcher functions. The user interface is updated to bind directly to the new state property and to respect a strict checking mode. Corresponding tests for multi-select tree and base select dropdowns are enhanced to cover more interactive scenarios and to use more robust element selectors. Metadata properties are removed from documentation and menu configuration files.

Changes

Files / Areas Changed Summary
packages/renderless/src/tree-select/index.ts Refactored selection logic: getCheckedData now takes selected as a parameter; added getChildValue utility and watchValue watcher for multi-select synchronization and parent/child node management.
packages/renderless/src/tree-select/vue.ts Updated state to use modelValue; imported and integrated new watcher and utility; added watchers for syncing model value changes.
packages/vue/src/tree-select/src/pc.vue Changed base select binding to state.modelValue; added check-strictly prop binding to tree component.
examples/sites/demos/pc/app/tree-select/multiple.spec.ts Expanded and refined multi-select tree test: added more steps and assertions for selection, deselection, and tag removal.
examples/sites/demos/pc/app/base-select/automatic-dropdown.spec.ts Changed dropdown locator from selecting the second instance to all instances, improving selector robustness.
examples/sites/demos/pc/app/base-select/slot-empty.spec.ts Refactored test to reuse an existing dropdown locator variable for consistency.
examples/sites/demos/pc/app/base-select/slot-reference.spec.ts Updated test to use semantic role-based locator for selecting dropdown options, improving accessibility and reliability.
examples/sites/demos/pc/app/tree-select/webdoc/tree-select.js
examples/sites/demos/pc/menus.js
Removed meta.experimental property from documentation and menu entries for the tree-select component.
packages/renderless/src/base-select/index.ts Modified setSelected logic to update state.selected only if the component instance does not have a panel slot, refining selection state updates.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant BaseSelect
    participant TreeSelect
    participant TreePanel

    User->>BaseSelect: Interacts with dropdown/tags
    BaseSelect->>TreeSelect: Updates state.modelValue
    TreeSelect->>TreeSelect: watchValue detects changes
    TreeSelect->>TreePanel: Sync checked keys (add/remove)
    TreePanel->>TreeSelect: Returns updated selection
    TreeSelect->>BaseSelect: Updates input/tags display
Loading

Suggested labels

enhancement

Suggested reviewers

  • zzcr

Poem

In the garden of trees, selections bloom anew,
With watchers and children, the logic grew.
Tags may come, and tags may go,
But strict checks keep the flow.
Tests now dance with careful grace,
As rabbits hop through every case!
🐇🌳✨

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

examples/sites/demos/pc/app/base-select/automatic-dropdown.spec.ts

Oops! Something went wrong! :(

ESLint: 8.57.1

ESLint couldn't find the plugin "eslint-plugin-vue".

(The package "eslint-plugin-vue" was not found when loaded as a Node module from the directory "".)

It's likely that the plugin isn't installed correctly. Try reinstalling by running the following:

npm install eslint-plugin-vue@latest --save-dev

The plugin "eslint-plugin-vue" was referenced from the config file in ".eslintrc.js » @antfu/eslint-config » @antfu/eslint-config-vue".

If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team.

examples/sites/demos/pc/app/base-select/slot-empty.spec.ts

Oops! Something went wrong! :(

ESLint: 8.57.1

ESLint couldn't find the plugin "eslint-plugin-vue".

(The package "eslint-plugin-vue" was not found when loaded as a Node module from the directory "".)

It's likely that the plugin isn't installed correctly. Try reinstalling by running the following:

npm install eslint-plugin-vue@latest --save-dev

The plugin "eslint-plugin-vue" was referenced from the config file in ".eslintrc.js » @antfu/eslint-config » @antfu/eslint-config-vue".

If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team.

examples/sites/demos/pc/app/base-select/slot-reference.spec.ts

Oops! Something went wrong! :(

ESLint: 8.57.1

ESLint couldn't find the plugin "eslint-plugin-vue".

(The package "eslint-plugin-vue" was not found when loaded as a Node module from the directory "".)

It's likely that the plugin isn't installed correctly. Try reinstalling by running the following:

npm install eslint-plugin-vue@latest --save-dev

The plugin "eslint-plugin-vue" was referenced from the config file in ".eslintrc.js » @antfu/eslint-config » @antfu/eslint-config-vue".

If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team.

  • 2 others
✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@github-actions github-actions bot added the bug Something isn't working label Apr 22, 2025
Copy link

Walkthrough

This PR addresses an issue in the tree-select component where the deleteTag functionality was not working. The changes involve refactoring the way selected data is handled, ensuring that the component correctly updates and reflects the selected state, especially when tags are deleted.

Changes

File Summary
packages/renderless/src/tree-select/index.ts Refactored getCheckedData to accept a selected parameter. Added getChildValue to recursively fetch child node IDs. Updated mounted and added watchValue to handle changes in selected values.
packages/renderless/src/tree-select/vue.ts Updated imports to include new methods. Replaced state.value with state.modelValue and added watchers for modelValue.
packages/vue/src/tree-select/src/pc.vue Changed v-model from state.value to state.modelValue and added check-strictly binding to tiny-tree.

let checkedKeys = newValue

// 处理输入框中删除选中标签时,联动下拉面板的逻辑
if (xorResult.length === 1 && !props.treeOp.checkStrictly) {

Choose a reason for hiding this comment

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

Ensure that node is not null before accessing node.isLeaf to prevent potential runtime errors.

Copy link

[e2e-test-warn]
The component to be tested is missing.

The title of the Pull request should look like "fix(vue-renderless): [action-menu, alert] fix xxx bug".

Please make sure you've read our contributing guide

@kagol kagol changed the title fix(tree-select): fix deleteTag not working fix(tree-select): [tree-select] fix deleteTag not working Apr 22, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (5)
packages/vue/src/tree-select/src/pc.vue (1)

14-14: Consider giving checkStrictly an explicit default

treeOp.checkStrictly may be undefined when the consumer does not supply the flag. Vue will still add a check-strictly attribute with value "undefined" to the DOM, which is truthy in HTML and could accidentally switch the tree into strict‑mode in some browsers or unit tests.

-        :check-strictly="treeOp.checkStrictly"
+        :check-strictly="Boolean(treeOp.checkStrictly)"

This keeps the prop purely boolean.

packages/renderless/src/tree-select/vue.ts (3)

1-11: Imported helpers are not exposed through the public api array

You correctly attach watchValue & getChildValue to the api object (lines 35‑38) yet export const api = [...] (line 13, unchanged) still omits them.
If external code (tests, wrappers, composables) relied on accessing those helpers it will now break.

-export const api = ['state', 'check', 'filter', 'nodeClick']
+export const api = [
+  'state',
+  'check',
+  'filter',
+  'nodeClick',
+  // newly added public helpers
+  'watchValue',
+  'getChildValue'
+]

If the omission is intentional, please add a comment clarifying that these helpers are private.


46-56: Redundant deep‑watch on primitives

props.modelValue can be a scalar (single‑select) or an array (multi‑select).
Deep‑watching scalars does nothing but still incurs overhead. You can scope the deep option to array input only:

-  watch(
+  watch(
     () => props.modelValue,
     () => { … },
-    { immediate: true, deep: true }
+    { immediate: true, deep: Array.isArray(props.modelValue) }
   )

Minor optimisation, feel free to skip if perf is a non‑issue.


58-58: Race‑condition safeguard

watch(() => state.modelValue, api.watchValue) executes after the previous watcher mutates state.modelValue.
Because both run synchronously it is possible for api.watchValue to receive identical newValue/oldValue arrays (Vue compares by reference).
Consider adding the flush: "post" option or an early exit (if (newValue === oldValue) return) inside watchValue to avoid redundant work.

packages/renderless/src/tree-select/index.ts (1)

139-155: Optimise getChildValue to avoid repeated push on large trees

For deep trees ids.push in every recursion may hit V8’s optimisation limit. Collect in a local array and concatenate once:

-  const ids = []
-  const getChild = (nodes) => {
-    nodes.forEach((node) => {
-      ids.push(node.data[key])
+  const ids = []
+  const getChild = (nodes) => {
+    nodes.forEach((node) => {
+      ids[ids.length] = node.data[key]
       …
     })
   }

Micro‑optimisation; optional.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b0aaef7 and 62dfaa6.

📒 Files selected for processing (3)
  • packages/renderless/src/tree-select/index.ts (2 hunks)
  • packages/renderless/src/tree-select/vue.ts (4 hunks)
  • packages/vue/src/tree-select/src/pc.vue (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
packages/renderless/src/tree-select/vue.ts (1)
packages/renderless/src/tree-select/index.ts (3)
  • nodeClick (15-33)
  • watchValue (198-248)
  • getChildValue (139-155)
packages/renderless/src/tree-select/index.ts (1)
packages/renderless/src/tree-select/vue.ts (1)
  • api (13-13)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: PR E2E Test (pnpm test:e2e3)
🔇 Additional comments (1)
packages/vue/src/tree-select/src/pc.vue (1)

5-5: Binding moved to state.modelValue looks correct

✅ The component now stays in sync with the new render‑less state and the watcher chain added in vue.ts. No further action required.

Comment on lines +198 to +208
export const watchValue =
({ api, props, vm, state }) =>
(newValue, oldValue) => {
if (props.multiple) {
// 取新旧值的差集,用来判断是否是删除标签的操作,如果差值只有一个值,说明是删除操作
// 如果是删除操作,且不是父子严格模式,则需要将父节点的值也删除(严格模式下父子节点勾选相互独立,不会相互影响)
const xorResult = oldValue.filter((item) => !newValue.includes(item))
const tagId = xorResult[0]
const treeIds = [tagId]
let checkedKeys = newValue

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Missing guard when oldValue is undefined

oldValue.filter will crash on the very first change (when oldValue is undefined). Defensive‑copy both arrays first:

-      const xorResult = oldValue.filter((item) => !newValue.includes(item))
+      const prev = Array.isArray(oldValue) ? oldValue : []
+      const xorResult = prev.filter((item) => !newValue.includes(item))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const watchValue =
({ api, props, vm, state }) =>
(newValue, oldValue) => {
if (props.multiple) {
// 取新旧值的差集,用来判断是否是删除标签的操作,如果差值只有一个值,说明是删除操作
// 如果是删除操作,且不是父子严格模式,则需要将父节点的值也删除(严格模式下父子节点勾选相互独立,不会相互影响)
const xorResult = oldValue.filter((item) => !newValue.includes(item))
const tagId = xorResult[0]
const treeIds = [tagId]
let checkedKeys = newValue
export const watchValue =
({ api, props, vm, state }) =>
(newValue, oldValue) => {
if (props.multiple) {
// 取新旧值的差集,用来判断是否是删除标签的操作,如果差值只有一个值,说明是删除操作
// 如果是删除操作,且不是父子严格模式,则需要将父节点的值也删除(严格模式下父子节点勾选相互独立,不会相互影响)
const prev = Array.isArray(oldValue) ? oldValue : []
const xorResult = prev.filter((item) => !newValue.includes(item))
const tagId = xorResult[0]
const treeIds = [tagId]
let checkedKeys = newValue

Comment on lines +118 to 130
(selected) => {
const checkedKey = []

if (!Array.isArray(state.selected)) {
return props.modelValue ? [props.modelValue] : [state.selected[props.valueField]]
if (!Array.isArray(selected)) {
return props.modelValue ? [props.modelValue] : [selected[props.valueField]]
} else {
state.selected.length > 0 &&
state.selected.forEach((item) => {
selected.length > 0 &&
selected.forEach((item) => {
checkedKey.push(item[props.valueField])
})

return checkedKey
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

getCheckedData may throw when selected is falsy

When selected is undefined or null and props.modelValue is also falsy, [selected[props.valueField]] will access a property of undefined, raising a runtime error.

-  if (!Array.isArray(selected)) {
-    return props.modelValue ? [props.modelValue] : [selected[props.valueField]]
+  if (!Array.isArray(selected)) {
+    if (props.modelValue) {
+      return [props.modelValue]
+    }
+    if (selected) {
+      return [selected[props.valueField]]
+    }
+    return []
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
(selected) => {
const checkedKey = []
if (!Array.isArray(state.selected)) {
return props.modelValue ? [props.modelValue] : [state.selected[props.valueField]]
if (!Array.isArray(selected)) {
return props.modelValue ? [props.modelValue] : [selected[props.valueField]]
} else {
state.selected.length > 0 &&
state.selected.forEach((item) => {
selected.length > 0 &&
selected.forEach((item) => {
checkedKey.push(item[props.valueField])
})
return checkedKey
}
(selected) => {
const checkedKey = []
if (!Array.isArray(selected)) {
if (props.modelValue) {
return [props.modelValue]
}
if (selected) {
return [selected[props.valueField]]
}
return []
} else {
selected.length > 0 &&
selected.forEach((item) => {
checkedKey.push(item[props.valueField])
})
return checkedKey
}

Comment on lines 160 to 186
if (!state.modelValue || state.modelValue.length === 0) return

if (props.multiple) {
let initialNodes = []
if (Array.isArray(state.value)) {
state.value.forEach((value) => {
if (Array.isArray(state.modelValue)) {
state.modelValue.forEach((value) => {
const option = api.getPluginOption(value)
initialNodes = initialNodes.concat(option)
})
}

vm.$refs.baseSelectRef.updateSelectedData(
initialNodes.map((node) => {
return {
...node,
currentLabel: node[props.textField],
value: node[props.valueField],
isTree: true
}
})
)
const selected = initialNodes.map((node) => {
return {
...node,
currentLabel: node[props.textField],
value: node[props.valueField],
isTree: true
}
})

state.defaultCheckedKeys = api.getCheckedData()[0]
vm.$refs.baseSelectRef.updateSelectedData(selected)

state.defaultCheckedKeys = api.getCheckedData(selected)
} else {
const data = api.getPluginOption(state.value)[0]
const data = api.getPluginOption(state.modelValue)[0]
vm.$refs.baseSelectRef.updateSelectedData({
...data,
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Early‑return hides single‑select initialisation

if (!state.modelValue || state.modelValue.length === 0) return short‑circuits both single‑ and multi‑select modes.
For single‑select state.modelValue is a scalar and you still need the fallback logic below.
Suggest splitting the condition:

-    if (!state.modelValue || state.modelValue.length === 0) return
+    if (props.multiple && (!state.modelValue || state.modelValue.length === 0)) {
+      return
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!state.modelValue || state.modelValue.length === 0) return
if (props.multiple) {
let initialNodes = []
if (Array.isArray(state.value)) {
state.value.forEach((value) => {
if (Array.isArray(state.modelValue)) {
state.modelValue.forEach((value) => {
const option = api.getPluginOption(value)
initialNodes = initialNodes.concat(option)
})
}
vm.$refs.baseSelectRef.updateSelectedData(
initialNodes.map((node) => {
return {
...node,
currentLabel: node[props.textField],
value: node[props.valueField],
isTree: true
}
})
)
const selected = initialNodes.map((node) => {
return {
...node,
currentLabel: node[props.textField],
value: node[props.valueField],
isTree: true
}
})
state.defaultCheckedKeys = api.getCheckedData()[0]
vm.$refs.baseSelectRef.updateSelectedData(selected)
state.defaultCheckedKeys = api.getCheckedData(selected)
} else {
const data = api.getPluginOption(state.value)[0]
const data = api.getPluginOption(state.modelValue)[0]
vm.$refs.baseSelectRef.updateSelectedData({
...data,
// Only skip when in multiple‑select with no values
if (props.multiple && (!state.modelValue || state.modelValue.length === 0)) {
return
}
if (props.multiple) {
let initialNodes = []
if (Array.isArray(state.modelValue)) {
state.modelValue.forEach((value) => {
const option = api.getPluginOption(value)
initialNodes = initialNodes.concat(option)
})
}
const selected = initialNodes.map((node) => {
return {
...node,
currentLabel: node[props.textField],
value: node[props.valueField],
isTree: true
}
})
vm.$refs.baseSelectRef.updateSelectedData(selected)
state.defaultCheckedKeys = api.getCheckedData(selected)
} else {
const data = api.getPluginOption(state.modelValue)[0]
vm.$refs.baseSelectRef.updateSelectedData({
...data,

Comment on lines +209 to +224
// 处理输入框中删除选中标签时,联动下拉面板的逻辑
if (xorResult.length === 1 && !props.treeOp.checkStrictly) {
let node = vm.$refs.treeRef.getNode(tagId)

if (!node.isLeaf) {
treeIds.push(...api.getChildValue(node.childNodes, props.valueField))
}

while (node.parent && !Array.isArray(node.parent.data)) {
node.parent.data && treeIds.push(node.parent.data[props.valueField])
node = node.parent
}

checkedKeys = newValue.filter((item) => !treeIds.includes(item))
}

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

getNode may return null

When the removed tag is not present in the current tree (lazy load / remote), vm.$refs.treeRef.getNode(tagId) returns null, causing node.isLeaf to throw.

-        let node = vm.$refs.treeRef.getNode(tagId)
-        if (!node.isLeaf) {
+        const node = vm.$refs.treeRef.getNode(tagId)
+        if (!node) {
+          checkedKeys = newValue   // silently ignore; tree not loaded yet
+        } else if (!node.isLeaf) {
           treeIds.push(...api.getChildValue(node.childNodes, props.valueField))
         }
-        while (node.parent && !Array.isArray(node.parent.data)) {
+        while (node && node.parent && !Array.isArray(node.parent.data)) {

Prevents hard crashes in remote‑data scenarios.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 处理输入框中删除选中标签时,联动下拉面板的逻辑
if (xorResult.length === 1 && !props.treeOp.checkStrictly) {
let node = vm.$refs.treeRef.getNode(tagId)
if (!node.isLeaf) {
treeIds.push(...api.getChildValue(node.childNodes, props.valueField))
}
while (node.parent && !Array.isArray(node.parent.data)) {
node.parent.data && treeIds.push(node.parent.data[props.valueField])
node = node.parent
}
checkedKeys = newValue.filter((item) => !treeIds.includes(item))
}
// 处理输入框中删除选中标签时,联动下拉面板的逻辑
if (xorResult.length === 1 && !props.treeOp.checkStrictly) {
const node = vm.$refs.treeRef.getNode(tagId)
if (!node) {
checkedKeys = newValue // silently ignore; tree not loaded yet
} else if (!node.isLeaf) {
treeIds.push(...api.getChildValue(node.childNodes, props.valueField))
}
while (node && node.parent && !Array.isArray(node.parent.data)) {
node.parent.data && treeIds.push(node.parent.data[props.valueField])
node = node.parent
}
checkedKeys = newValue.filter((item) => !treeIds.includes(item))
}

@kagol kagol force-pushed the kagol/fix-tree-select-delete-tag branch from 62dfaa6 to 29977fa Compare April 22, 2025 06:34
@kagol kagol changed the title fix(tree-select): [tree-select] fix deleteTag not working fix(tree-select): [base-select, tree-select] fix deleteTag not working Apr 22, 2025
@kagol kagol force-pushed the kagol/fix-tree-select-delete-tag branch from 80da722 to c8c7fa8 Compare April 22, 2025 09:39
@kagol kagol force-pushed the kagol/fix-tree-select-delete-tag branch from c8c7fa8 to 4160f1f Compare April 22, 2025 09:41
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (4)
packages/renderless/src/tree-select/index.ts (4)

159-159: Risk of skipping single-select initialization

The early return condition might skip initialization for single-select mode where modelValue is a scalar value, not an array.

-if (!state.modelValue || state.modelValue.length === 0) return
+if (props.multiple && (!state.modelValue || state.modelValue.length === 0)) {
+  return
+}

117-121: ⚠️ Potential issue

Fix null handling in getCheckedData

The function doesn't properly handle the case when both selected is falsy and props.modelValue is falsy. Accessing selected[props.valueField] will throw an error if selected is undefined or null.

-  return props.modelValue ? [props.modelValue] : [selected[props.valueField]]
+  if (props.modelValue) {
+    return [props.modelValue]
+  }
+  return selected ? [selected[props.valueField]] : []

196-204: ⚠️ Potential issue

Fix potential crash when oldValue is undefined

The line that calculates xorResult will throw an error if oldValue is undefined (which can happen on the first render).

-const xorResult = oldValue.filter((item) => !newValue.includes(item))
+const prevValue = Array.isArray(oldValue) ? oldValue : []
+const xorResult = prevValue.filter((item) => !newValue.includes(item))

207-218: ⚠️ Potential issue

Add null checking for tree node

The code doesn't verify if node exists before accessing node.isLeaf and in the while loop, which could lead to runtime errors if the node isn't found in the tree.

-let node = vm.$refs.treeRef.getNode(tagId)
-
-if (!node.isLeaf) {
+const node = vm.$refs.treeRef.getNode(tagId)
+
+if (!node) {
+  // Handle case where node isn't found (e.g., during lazy loading)
+  checkedKeys = newValue
+} else if (!node.isLeaf) {
  treeIds.push(...api.getChildValue(node.childNodes, props.valueField))
}

-while (node.parent && !Array.isArray(node.parent.data)) {
+while (node && node.parent && !Array.isArray(node.parent.data)) {
🧹 Nitpick comments (1)
packages/renderless/src/tree-select/index.ts (1)

132-154: Consider reusing existing getChildValue function

This function appears nearly identical to one already defined in packages/renderless/src/base-select/index.ts. Consider importing the existing implementation to reduce duplication.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c8c7fa8 and 4160f1f.

📒 Files selected for processing (5)
  • examples/sites/demos/pc/app/base-select/automatic-dropdown.spec.ts (1 hunks)
  • examples/sites/demos/pc/app/base-select/slot-empty.spec.ts (1 hunks)
  • examples/sites/demos/pc/app/base-select/slot-reference.spec.ts (1 hunks)
  • packages/renderless/src/base-select/index.ts (1 hunks)
  • packages/renderless/src/tree-select/index.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • examples/sites/demos/pc/app/base-select/slot-empty.spec.ts
  • examples/sites/demos/pc/app/base-select/automatic-dropdown.spec.ts
  • packages/renderless/src/base-select/index.ts
  • examples/sites/demos/pc/app/base-select/slot-reference.spec.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
packages/renderless/src/tree-select/index.ts (4)
packages/vue-common/src/index.ts (1)
  • props (53-61)
packages/renderless/src/base-select/index.ts (3)
  • getChildValue (1368-1384)
  • mounted (1593-1636)
  • watchValue (1082-1115)
packages/renderless/src/tree-select/vue.ts (1)
  • api (13-13)
packages/vue-hooks/src/vue-emitter.ts (1)
  • vm (15-49)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: PR E2E Test (pnpm test:e2e3)
🔇 Additional comments (3)
packages/renderless/src/tree-select/index.ts (3)

51-51: Removed tree identification flag

The removal of isTree: true from the selected node objects is part of the fix for the deleteTag issue, allowing the base-select component to handle tree selections more consistently.


163-181: Looks good: Proper initialization with modelValue

The changes appropriately handle mapping modelValue to the data structure needed by the base-select component during initialization.


219-244: Good implementation of tag deletion synchronization

This code effectively handles the complex case of deleting tags in a tree structure, maintaining consistency between the selected tags and the tree's checked nodes. This appears to be the core fix for the "deleteTag not working" issue mentioned in the PR objectives.

@zzcr zzcr merged commit 0cc6f28 into dev Apr 22, 2025
7 of 10 checks passed
@zzcr zzcr deleted the kagol/fix-tree-select-delete-tag branch April 22, 2025 09:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants