-
Notifications
You must be signed in to change notification settings - Fork 321
fix(tree-select): [base-select, tree-select] fix deleteTag not working #3332
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
WalkthroughThis 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 Changes
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
Suggested labels
Suggested reviewers
Poem
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
examples/sites/demos/pc/app/base-select/automatic-dropdown.spec.tsOops! 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:
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.tsOops! 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:
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.tsOops! 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:
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.
✨ Finishing Touches
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
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)
Other keywords and placeholders
CodeRabbit Configuration File (
|
WalkthroughThis PR addresses an issue in the Changes
|
let checkedKeys = newValue | ||
|
||
// 处理输入框中删除选中标签时,联动下拉面板的逻辑 | ||
if (xorResult.length === 1 && !props.treeOp.checkStrictly) { |
There was a problem hiding this comment.
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.
[e2e-test-warn] 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 |
There was a problem hiding this 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 givingcheckStrictly
an explicit default
treeOp.checkStrictly
may beundefined
when the consumer does not supply the flag. Vue will still add acheck-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 publicapi
arrayYou correctly attach
watchValue
&getChildValue
to theapi
object (lines 35‑38) yetexport 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 thedeep
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 mutatesstate.modelValue
.
Because both run synchronously it is possible forapi.watchValue
to receive identicalnewValue
/oldValue
arrays (Vue compares by reference).
Consider adding theflush: "post"
option or an early exit (if (newValue === oldValue) return
) insidewatchValue
to avoid redundant work.packages/renderless/src/tree-select/index.ts (1)
139-155
: OptimisegetChildValue
to avoid repeatedpush
on large treesFor 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
📒 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 tostate.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.
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 | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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 |
(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 | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
(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 | |
} |
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, |
There was a problem hiding this comment.
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.
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, |
// 处理输入框中删除选中标签时,联动下拉面板的逻辑 | ||
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)) | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
// 处理输入框中删除选中标签时,联动下拉面板的逻辑 | |
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)) | |
} |
62dfaa6
to
29977fa
Compare
80da722
to
c8c7fa8
Compare
c8c7fa8
to
4160f1f
Compare
There was a problem hiding this 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 initializationThe 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 issueFix null handling in getCheckedData
The function doesn't properly handle the case when both
selected
is falsy andprops.modelValue
is falsy. Accessingselected[props.valueField]
will throw an error ifselected
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 issueFix potential crash when oldValue is undefined
The line that calculates
xorResult
will throw an error ifoldValue
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 issueAdd null checking for tree node
The code doesn't verify if
node
exists before accessingnode.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 functionThis 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
📒 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 flagThe 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 modelValueThe changes appropriately handle mapping
modelValue
to the data structure needed by the base-select component during initialization.
219-244
: Good implementation of tag deletion synchronizationThis 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.
PR
PR Checklist
Please check if your PR fulfills the following requirements:
PR Type
What kind of change does this PR introduce?
What is the current behavior?
Issue Number: N/A
What is the new behavior?
Does this PR introduce a breaking change?
Other information
Summary by CodeRabbit
New Features
Bug Fixes
Tests
Chores