Skip to content

fix(SmaeTable): chavear expansão de sub-linhas por id em vez de índice#594

Open
Eduruiz wants to merge 1 commit intohomolfrom
fix/smae-table-sublinha-key-por-id
Open

fix(SmaeTable): chavear expansão de sub-linhas por id em vez de índice#594
Eduruiz wants to merge 1 commit intohomolfrom
fix/smae-table-sublinha-key-por-id

Conversation

@Eduruiz
Copy link
Collaborator

@Eduruiz Eduruiz commented Feb 26, 2026

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

  • New Features
    • Introduced a configurable field identifier for table rows, enabling customization of which field serves as a unique row identifier. Enhanced row expansion behavior to correctly maintain expansion states when table data is reordered or dynamically updated.

…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.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 26, 2026

📝 Walkthrough

Walkthrough

The changes add a configurable campoId prop to SmaeTable and TableBody components to specify which field serves as a row identifier. The TableBody component refactors its row expansion state tracking from index-based to ID-based logic, including new helper methods and an updated watcher.

Changes

Cohort / File(s) Summary
SmaeTable Prop Threading
frontend/src/components/SmaeTable/SmaeTable.vue
Added new public prop campoId?: string with default value 'id', threaded from SmaeTable to TableBody component and passed through to slot bindings.
TableBody ID-Based Expansion Refactor
frontend/src/components/SmaeTable/partials/TableBody.vue
Added campoId prop, converted internal expansion state from index-based to ID-based tracking via linhasExpandidas map, introduced helper methods obterIdDaLinha() and linhaEstaExpandida(), updated watcher to track row IDs instead of indices, and refactored toggle logic and template references to use ID-based lookups.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • GustavoFSoares
  • robsonsobral

Poem

🐰 A table that remembers each row by name,
No longer counting by index—what a game!
The campo identifier brings order and grace,
Expansion now rooted in each item's true place.
Hop hop, data dances with identity's gleam! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main change: fixing SmaeTable's sub-line expansion tracking to use id instead of index, which directly addresses the problem of expansion state misalignment when data is filtered or reordered.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/smae-table-sublinha-key-por-id

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

Comment @coderabbitai help to get the list of available commands and usage tips.

@sonarqubecloud
Copy link

Copy link
Contributor

@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: 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

📥 Commits

Reviewing files that changed from the base of the PR and between 3a68b1e and 83d58e3.

📒 Files selected for processing (2)
  • frontend/src/components/SmaeTable/SmaeTable.vue
  • frontend/src/components/SmaeTable/partials/TableBody.vue

Comment on lines +74 to 89
() => [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 },
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 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:


🌐 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 !==). So watch(() => state.obj, ...) won’t fire for state.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, newValue and oldValue typically point to the same object (Vue doesn’t snapshot the old structure). [1]

  • Reactive object as the source (watch(obj, ...)) is implicitly deep: passing a reactive() 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: 1 to trigger on array mutation without deeply traversing elements. [1][3]

Rule of thumb

  • Want “reference equality” behavior: watch(() => state.someObject, ...) (no deep). [1]
  • Want nested mutation tracking: watch(state.someObject, ...) (implicitly deep) or watch(() => 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 f

Repository: 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"
fi

Repository: 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.

Suggested change
() => [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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant