fix(SmaeTable): chavear expansão de sub-linhas por id em vez de índice#594
fix(SmaeTable): chavear expansão de sub-linhas por id em vez de índice#594
Conversation
…z de índice Evita que filtrar ou reordenar os dados externamente faça o toggle apontar para a linha errada. Adiciona prop `campoId` (default 'id') para permitir indicar qual campo usar como identificador.
📝 WalkthroughWalkthroughThe changes add a configurable Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/components/SmaeTable/partials/TableBody.vue`:
- Around line 74-89: The watcher on props.subLinhaAbertaPorPadrao and
props.dados is shallow so in-place mutations to props.dados (push/splice/sort)
won't trigger it; update the watch call that references
props.subLinhaAbertaPorPadrao and props.dados to run in deep mode (add deep:
true to the options object alongside immediate: true) so changes inside the
dados array are detected and the logic that uses obterIdDaLinha and updates
linhasExpandidas runs for mutated arrays.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
frontend/src/components/SmaeTable/SmaeTable.vuefrontend/src/components/SmaeTable/partials/TableBody.vue
| () => [props.subLinhaAbertaPorPadrao, props.dados] as const, | ||
| ([abertaPorPadrao, dados]) => { | ||
| if (!abertaPorPadrao || dados.length === 0) return; | ||
|
|
||
| const indicesNovos = Array.from({ length: tamanho }, (_, i) => i) | ||
| .filter((i) => linhasExpandidas.value[i] === undefined); | ||
| const idsNovos = dados | ||
| .map((item) => obterIdDaLinha(item)) | ||
| .filter((id) => linhasExpandidas.value[id] === undefined); | ||
|
|
||
| if (indicesNovos.length) { | ||
| if (idsNovos.length) { | ||
| linhasExpandidas.value = { | ||
| ...linhasExpandidas.value, | ||
| ...Object.fromEntries(indicesNovos.map((i) => [i, true])), | ||
| ...Object.fromEntries(idsNovos.map((id) => [id, true])), | ||
| }; | ||
| } | ||
| }, | ||
| { immediate: true }, |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Vue 3.5 watch getter array in-place mutations push splice trigger callback
💡 Result:
In Vue 3.5, watch(() => arr) (or watch(() => arr.value)) is shallow by default: it only re-runs when the array reference changes, so in-place mutations like push() / splice() won’t trigger the callback unless you opt into deep watching. Vue’s docs describe this as “watch is shallow by default”; deep watching is required for nested mutations. [1]
Fixes (pick one)
A) Deep watch the array
const list = ref([])
watch(
() => list.value,
() => { /* will run on push/splice */ },
{ deep: true }
)Deep watchers trigger on nested mutations. [1]
B) If it’s a reactive array/object, watch it directly (implicit deep)
const list = reactive([])
watch(list, () => { /* runs on push/splice */ })Watching a reactive object directly creates an implicit deep watcher. [1]
C) Watch a specific derived value (often cheaper than deep)
watch(() => list.value.length, () => { /* runs on push/splice */ })D) If you want added/removed elements, use VueUse watchArray (+ deep for in-place edits)
watchArray can tell you what was added/removed, and it notes you should pass { deep: true } when mutating in place with push/splice. [2]
Sources: Vue watchers / deep watchers behavior [1], VueUse watchArray note about { deep: true } for push/splice [2].
[1] (vuejs.org)
[2] (vueuse.org)
Citations:
- 1: https://vuejs.org/guide/essentials/watchers?utm_source=openai
- 2: https://vueuse.org/shared/watcharray/?utm_source=openai
🌐 Web query:
Vue 3 composition API watch() reference equality vs deep tracking
💡 Result:
In Vue 3 Composition API, watch() triggers based on what you pass as the source:
-
Getter source (
() => something) is shallow by default: the callback runs only when the getter’s return value changes by identity (roughly!==). Sowatch(() => state.obj, ...)won’t fire forstate.obj.nested++unless you enable deep watching. [1] -
Deep watching (
{ deep: true }or{ deep: N }in Vue 3.5+) makes Vue traverse nested properties so mutations inside the object/array also trigger the watcher. On deep mutations,newValueandoldValuetypically point to the same object (Vue doesn’t snapshot the old structure). [1] -
Reactive object as the source (
watch(obj, ...)) is implicitly deep: passing areactive()object directly creates a deep watcher automatically. [1][2] -
Arrays: watching an array value is shallow unless deep is enabled; in Vue 3.5+ the migration guide recommends
deep: 1to trigger on array mutation without deeply traversing elements. [1][3]
Rule of thumb
- Want “reference equality” behavior:
watch(() => state.someObject, ...)(nodeep). [1] - Want nested mutation tracking:
watch(state.someObject, ...)(implicitly deep) orwatch(() => state.someObject, ..., { deep: true | N }). [1][2]
Sources: Vue Watchers Guide [1], Vue watch() API docs [2], Vue 3 migration note on watching arrays [3].
[1] https://vuejs.org/guide/essentials/watchers.html
[2] https://vuejs.org/api/reactivity-core.html
[3] https://v3-migration.vuejs.org/breaking-changes/watch.html
🌐 Web query:
Vue 3.5 watcher documentation props.dados array mutation behavior
💡 Result:
In Vue 3 (including 3.5), a watcher is shallow by default: if you watch an array via a getter like watch(() => props.dados, ...), the callback runs only when the array reference is replaced, not when it’s mutated with push/splice/sort, etc. To react to array mutations, you must use a deep watcher. In Vue 3.5+, prefer deep: 1 so it triggers on array replacement + array mutations without deeply watching inside each element object. [1][2]
watch(
() => props.dados,
(newVal, oldVal) => { /* ... */ },
{ deep: 1 } // Vue 3.5+
)Also note: props are shallow readonly—you can’t reassign props.dados, but nested values (like the array contents) are not made readonly by shallowReadonly, so mutating props.dados.push(...) can still “work” (and will mutate parent state), even though it’s an anti-pattern. [3]
Sources: Vue Watchers guide (deep + numeric depth) [1], Vue 3 “Watch on Arrays” breaking change / deep: 1 in 3.5+ [2], Vue shallowReadonly() behavior [3].
[1] https://vuejs.org/guide/essentials/watchers
[2] https://v3-migration.vuejs.org/breaking-changes/watch.html
[3] https://vuejs.org/api/reactivity-advanced
🏁 Script executed:
fd "TableBody.vue" --type fRepository: AppCivico/smae
Length of output: 114
🏁 Script executed:
if [ -f "frontend/src/components/SmaeTable/partials/TableBody.vue" ]; then
wc -l "frontend/src/components/SmaeTable/partials/TableBody.vue"
fiRepository: AppCivico/smae
Length of output: 118
🏁 Script executed:
sed -n '70,100p' "frontend/src/components/SmaeTable/partials/TableBody.vue"Repository: AppCivico/smae
Length of output: 928
Watcher won't detect in-place mutations to props.dados.
watch(() => [props.subLinhaAbertaPorPadrao, props.dados]) is shallow by default in Vue 3.5. In-place mutations like push(), splice(), or sort() won't trigger the callback, so new rows won't auto-expand when subLinhaAbertaPorPadrao is true.
🔧 Suggested fix
watch(
- () => [props.subLinhaAbertaPorPadrao, props.dados] as const,
- ([abertaPorPadrao, dados]) => {
- if (!abertaPorPadrao || dados.length === 0) return;
-
- const idsNovos = dados
- .map((item) => obterIdDaLinha(item))
+ () => [
+ props.subLinhaAbertaPorPadrao,
+ props.campoId,
+ props.dados.map((item) => obterIdDaLinha(item)),
+ ] as const,
+ ([abertaPorPadrao, _campoId, idsAtuais]) => {
+ if (!abertaPorPadrao || idsAtuais.length === 0) return;
+
+ const idsNovos = idsAtuais
.filter((id) => linhasExpandidas.value[id] === undefined);
if (idsNovos.length) {
linhasExpandidas.value = {
...linhasExpandidas.value,
...Object.fromEntries(idsNovos.map((id) => [id, true])),
};
}
},
{ immediate: true },
);📝 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.
| () => [props.subLinhaAbertaPorPadrao, props.dados] as const, | |
| ([abertaPorPadrao, dados]) => { | |
| if (!abertaPorPadrao || dados.length === 0) return; | |
| const indicesNovos = Array.from({ length: tamanho }, (_, i) => i) | |
| .filter((i) => linhasExpandidas.value[i] === undefined); | |
| const idsNovos = dados | |
| .map((item) => obterIdDaLinha(item)) | |
| .filter((id) => linhasExpandidas.value[id] === undefined); | |
| if (indicesNovos.length) { | |
| if (idsNovos.length) { | |
| linhasExpandidas.value = { | |
| ...linhasExpandidas.value, | |
| ...Object.fromEntries(indicesNovos.map((i) => [i, true])), | |
| ...Object.fromEntries(idsNovos.map((id) => [id, true])), | |
| }; | |
| } | |
| }, | |
| { immediate: true }, | |
| () => [ | |
| props.subLinhaAbertaPorPadrao, | |
| props.campoId, | |
| props.dados.map((item) => obterIdDaLinha(item)), | |
| ] as const, | |
| ([abertaPorPadrao, _campoId, idsAtuais]) => { | |
| if (!abertaPorPadrao || idsAtuais.length === 0) return; | |
| const idsNovos = idsAtuais | |
| .filter((id) => linhasExpandidas.value[id] === undefined); | |
| if (idsNovos.length) { | |
| linhasExpandidas.value = { | |
| ...linhasExpandidas.value, | |
| ...Object.fromEntries(idsNovos.map((id) => [id, true])), | |
| }; | |
| } | |
| }, | |
| { immediate: true }, |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/components/SmaeTable/partials/TableBody.vue` around lines 74 -
89, The watcher on props.subLinhaAbertaPorPadrao and props.dados is shallow so
in-place mutations to props.dados (push/splice/sort) won't trigger it; update
the watch call that references props.subLinhaAbertaPorPadrao and props.dados to
run in deep mode (add deep: true to the options object alongside immediate:
true) so changes inside the dados array are detected and the logic that uses
obterIdDaLinha and updates linhasExpandidas runs for mutated arrays.



Evita que filtrar ou reordenar os dados externamente faça o toggle apontar para a linha errada. Adiciona prop
campoId(default 'id') para permitir indicar qual campo usar como identificador.Summary by CodeRabbit