Skip to content

fix: reload table after archive/unarchive rows#938

Open
tomrndom wants to merge 2 commits into
masterfrom
fix/reload-table-on-archive
Open

fix: reload table after archive/unarchive rows#938
tomrndom wants to merge 2 commits into
masterfrom
fix/reload-table-on-archive

Conversation

@tomrndom
Copy link
Copy Markdown

@tomrndom tomrndom commented May 14, 2026

ref: https://app.clickup.com/t/86b7v230m

Signed-off-by: Tomás Castillo tcastilloboireau@gmail.com

Summary by CodeRabbit

  • Bug Fixes
    • Improved pagination handling when archiving or unarchiving items across forms, templates, pages, and inventory lists. The system now correctly adjusts to a valid page instead of potentially displaying empty pages after removing items.

Review Change Stack

tomrndom added 2 commits May 14, 2026 16:44
Signed-off-by: Tomás Castillo <tcastilloboireau@gmail.com>
Signed-off-by: Tomás Castillo <tcastilloboireau@gmail.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 14, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5f13da35-55b8-4bd1-82de-20264e720c89

📥 Commits

Reviewing files that changed from the base of the PR and between 81351ea and 9484135.

📒 Files selected for processing (11)
  • src/pages/sponsors-global/form-templates/form-template-item-list-page.js
  • src/pages/sponsors-global/form-templates/form-template-list-page.js
  • src/pages/sponsors-global/inventory/inventory-list-page.js
  • src/pages/sponsors-global/page-templates/page-template-list-page.js
  • src/pages/sponsors/show-pages-list-page/index.js
  • src/pages/sponsors/sponsor-form-item-list-page/index.js
  • src/pages/sponsors/sponsor-forms-list-page/index.js
  • src/pages/sponsors/sponsor-page/tabs/sponsor-forms-tab/components/manage-items/sponsor-forms-manage-items.js
  • src/pages/sponsors/sponsor-page/tabs/sponsor-forms-tab/index.js
  • src/pages/sponsors/sponsor-page/tabs/sponsor-pages-tab/index.js
  • src/utils/methods.js

📝 Walkthrough

Walkthrough

This PR introduces a utility function to safely recalculate pagination page numbers after archiving or unarchiving list items, then applies it consistently across nine different page and tab components. The change prevents users from being left on invalid empty pages after removing items from the current view.

Changes

Safe Pagination After Archive/Unarchive

Layer / File(s) Summary
Safe page calculation utility
src/utils/methods.js
getSafePageAfterRemove(totalCount, perPage, currentPage) computes the maximum valid page after removing one item using Math.ceil((totalCount - 1) / perPage), enforces a minimum of page 1, and returns the smaller of the current page or that computed maximum.
Global sponsors archive handlers
src/pages/sponsors-global/form-templates/form-template-item-list-page.js, src/pages/sponsors-global/form-templates/form-template-list-page.js, src/pages/sponsors-global/inventory/inventory-list-page.js, src/pages/sponsors-global/page-templates/page-template-list-page.js
Each archive/unarchive handler now imports getSafePageAfterRemove, computes a safe page after the mutation resolves, and reloads the list using that adjusted page instead of the current page.
Individual sponsor pages archive handlers
src/pages/sponsors/show-pages-list-page/index.js, src/pages/sponsors/sponsor-forms-list-page/index.js, src/pages/sponsors/sponsor-form-item-list-page/index.js, src/pages/sponsors/sponsor-page/tabs/sponsor-forms-tab/index.js, src/pages/sponsors/sponsor-page/tabs/sponsor-pages-tab/index.js, src/pages/sponsors/sponsor-page/tabs/sponsor-forms-tab/components/manage-items/sponsor-forms-manage-items.js
Each archive/unarchive handler imports getSafePageAfterRemove, chains a .then() or wraps the reload logic to compute the safe page from total items, per-page count, and current page, then reloads with the adjusted page.

Sequence Diagram

sequenceDiagram
  participant User
  participant ListComponent
  participant ArchiveAPI
  participant getSafePageAfterRemove
  participant ReloadAPI
  
  User->>ListComponent: Click archive/unarchive item
  ListComponent->>ArchiveAPI: archiveItem() or unarchiveItem()
  ArchiveAPI-->>ListComponent: Promise resolves
  ListComponent->>getSafePageAfterRemove: Calculate safe page from (totalCount-1, perPage, currentPage)
  getSafePageAfterRemove-->>ListComponent: safePage (clamped to valid range)
  ListComponent->>ReloadAPI: fetchItems(page = safePage)
  ReloadAPI-->>ListComponent: Updated list
  ListComponent-->>User: Display updated list on valid page
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • fntechgit/summit-admin#778: Both PRs change src/pages/sponsors/sponsor-forms-tab/index.js to re-fetch the customized forms list after a mutation (main PR via getSafePageAfterRemove after archive/unarchive; retrieved PR by reloading after delete with pagination reset).
  • fntechgit/summit-admin#809: Both PRs change the sponsor pages tab's archive/unarchive flow and subsequent re-fetching of customized pages in src/pages/sponsors/sponsor-pages-tab/index.js (one adds the archive/unarchive Redux actions and dispatch, the other adjusts the page number after the mutation via getSafePageAfterRemove).
  • fntechgit/summit-admin#932: Both PRs modify the sponsor pages tab's post-archive/remove refresh of customized pages—main PR changes the pagination page via getSafePageAfterRemove, while the retrieved PR fixes the archived-filter argument passed.

Suggested reviewers

  • smarcet
  • martinquiroga-exo

Poem

🐰 When lists grow short and pages break,
A rabbit's math can always make
Safe bounds for pagination's stride—
No empty screens where items hide!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: reload table after archive/unarchive rows' accurately describes the main change: adding a utility function to compute safe pagination and applying it across multiple files when archiving/unarchiving items to prevent empty pages.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/reload-table-on-archive

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

@tomrndom tomrndom requested a review from smarcet May 14, 2026 20:49
Comment thread src/utils/methods.js
const totalPages = Math.ceil(totalAfter / perPage);
const lastValidPage = Math.max(1, totalPages);
return Math.min(currentPage, lastValidPage);
};
Copy link
Copy Markdown

@smarcet smarcet May 15, 2026

Choose a reason for hiding this comment

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

@tomrndom
Bug: safe-page logic fires unconditionally for both archive and unarchive, but the item only leaves the current view in one of the four direction × filter combinations.

showArchived Action Item leaves view? Decrement correct?
false (default) Archive ✅ Yes — archived items are hidden ✅ Yes
true Archive ❌ No — still visible (grayed out) ❌ No
true Unarchive ❌ No — still visible (un-grayed) ❌ No

Concrete example of the bug:
showArchived=true, totalCount=21, perPage=10, currentPage=3 (1 item on page 3).
User unarchives that item → getSafePageAfterRemove(21, 10, 3) returns 2 → user gets bounced to page 2, but the item is still visible (unarchiving never removes it from a showArchived=true list). Page 3 is not empty.

Suggested fix: move the "does this item leave the view?" decision inside this function, so the 10 call sites stay simple:

export const getSafePageAfterRemove = (
  totalCount,
  perPage,
  currentPage,
  isItemCurrentlyArchived, // item.is_archived — state BEFORE the action
  showArchived             // current filter value
) => {
  // Item only disappears from view when it is being archived (isItemCurrentlyArchived=false)
  // and the list is currently hiding archived items (showArchived=false).
  const itemLeavesView = !isItemCurrentlyArchived && !showArchived;
  if (!itemLeavesView) return currentPage;

  const totalAfter = totalCount - 1;
  const totalPages = Math.ceil(totalAfter / perPage);
  const lastValidPage = Math.max(1, totalPages);
  return Math.min(currentPage, lastValidPage);
};

Then every call site just adds the two extra args — item.is_archived is already in scope at each handler since the item.is_archived ? unarchive() : archive() branch is right above the .then():

const safePage = getSafePageAfterRemove(
  totalCount, perPage, currentPage, item.is_archived, showArchived
);

One exception to verify: the customizedPages handler in sponsor-forms-tab/index.js does not receive an item argument — confirm what is_archived value applies there, or whether showArchived governs that sub-list independently.

Copy link
Copy Markdown

@smarcet smarcet left a comment

Choose a reason for hiding this comment

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

@tomrndom please review comments

(item.is_archived
? unarchiveFormTemplate(item)
: archiveFormTemplate(item)
).then(() => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[MEDIUM] Missing .catch() — unhandled rejection on archive/unarchive failure

All 10 archive handlers in this PR follow the same pattern:

archiveAction(item.id).then(() => {
  const safePage = ...;
  getItems(...);
});
// no .catch()

If the API call fails, the action's own authErrorHandler fires a snackbar — that part is fine. But the bare rejected promise propagates up with no handler, producing an unhandled promise rejection warning. More subtly, there is no explicit guarantee that the follow-up getItems reload is skipped on failure — that is only implicitly guaranteed because .then() is not called on rejection.

Suggested fix — add a no-op catch to all 10 handlers:

archiveAction(item.id)
  .then(() => {
    const safePage = getSafePageAfterRemove(...); getItems(term, safePage, perPage, order, orderDir, showArchived); })
  .catch(() => {
    // API error already handled by authErrorHandler; reload current page to keep list consistent
    getItems(term, currentPage, perPage, order, orderDir, showArchived); }); ```

Reloading on failure in the catch also ensures the list stays in sync if the server rejected the action silently (e.g., a 403 the error handler swallowed).

@smarcet
Copy link
Copy Markdown

smarcet commented May 15, 2026

[HIGH] totalCount fed into getSafePageAfterRemove is stale — reducers never update it on archive/unarchive

Note: the reducers are not modified by this PR, so this is a pre-existing structural issue that this PR inherits. Flagging here because the correctness of getSafePageAfterRemove depends on totalCount being accurate.

What the reducers do today:

All FORM_TEMPLATE_ARCHIVED, FORM_TEMPLATE_UNARCHIVED, SPONSOR_CUSTOMIZED_FORM_ARCHIVED_CHANGED, etc. handlers only toggle is_archived on the matching item in the list. They do not decrement totalCount. Example from form-template-list-reducer.js:

case FORM_TEMPLATE_ARCHIVED: {
  const updatedFormTemplates = state.formTemplates.map((item) =>
    item.id === updatedFormTemplate.id ? { ...item, is_archived: true } : item
  );
  return { ...state, formTemplates: updatedFormTemplates };
  // ↑ totalFormTemplates is untouched
}

Compare with FORM_TEMPLATE_DELETED, which correctly decrements:

case FORM_TEMPLATE_DELETED:
  return { ...state, formTemplates: [...], totalFormTemplates: state.totalFormTemplates - 1 };

Why this matters for this PR:

getSafePageAfterRemove(totalFormTemplates, perPage, currentPage) reads totalFormTemplates from Redux — which is the count from the last RECEIVE response. So:

  1. User is on page 3, totalCount = 21, perPage = 10. Archives item A → safe page calculated correctly from 21 → reloads, totalCount becomes 20.
  2. Without a full reload completing first (rapid clicks), user archives item B → getSafePageAfterRemove still reads totalCount = 21 (the ARCHIVED action never decremented it) → safe page is off by one.

Under normal single-click usage this is masked by the follow-up getItems refreshing totalCount from the server. But it is a latent race: if the user archives two items faster than the first reload completes, the second page calculation is wrong.

Correct fix: decrement totalCount in each ARCHIVED reducer case (mirroring what DELETED already does), so the in-flight page calculation always uses the post-action count:

case FORM_TEMPLATE_ARCHIVED: {
  const updatedFormTemplates = state.formTemplates.map((item) =>
    item.id === updatedFormTemplate.id ? { ...item, is_archived: true } : item
  );
  return {
    ...state,
    formTemplates: updatedFormTemplates,
    totalFormTemplates: state.totalFormTemplates - 1  // ← add this
  };
}

This applies to all domains: form-template-list-reducer, inventory-list-reducer, show-pages-list-reducer, sponsor-forms-list-reducer, and sponsor-page-forms-list-reducer.


This finding is secondary to the direction×filter bug already commented on getSafePageAfterRemove — fix that one first.

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.

2 participants