Skip to content

feat: multi-column sort for ResourceTable#52

Closed
iamtanbirahmed wants to merge 4 commits into
mainfrom
claude/issue-49-20260512-2214
Closed

feat: multi-column sort for ResourceTable#52
iamtanbirahmed wants to merge 4 commits into
mainfrom
claude/issue-49-20260512-2214

Conversation

@iamtanbirahmed

@iamtanbirahmed iamtanbirahmed commented May 12, 2026

Copy link
Copy Markdown
Owner

Implements multi-column sort for the resource table as described in issue #49.

Changes:

  • Click column header to sort ascending; click again to reverse
  • Shift+Click adds secondary/tertiary sort (max 3 columns)
  • Sort indicators ▲/▼ shown in column headers with rank numbers for multi-sort
  • Sort resets on resource type change, maintained on refresh & view switch
  • 15 new tests

Closes #49

Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Resource table gains interactive multi-column sorting with ▲/▼ and rank indicators; Shift‑click adds secondary sorts (up to three).
  • Bug Fixes

    • Stable, case‑insensitive sorting; filtering applied before sorting. Sort state is preserved when column layout is unchanged and resets when columns change. Row rendering deferred for consistent updates.
  • Tests

    • Added comprehensive unit and integration tests for sorting, toggling, multi‑column rules, and state preservation.

Review Change Stack

- Track sort state via _sort_columns (list of (col_idx, reverse) tuples)
- Click column header: set as primary sort ascending; click again to toggle
- Shift+Click: add secondary/tertiary sort (max 3 columns)
- Sort indicators ▲/▼ displayed in column headers; multi-sort shows rank (▲1, ▲2)
- Stable multi-key sort using reversed-order stable sort trick
- Sort resets when resource type changes (different columns); maintained on refresh
- Add _compute_next_sort() as testable pure-logic helper
- Add 15 new tests covering sort logic, multi-column, edge cases, and UI behavior

Co-authored-by: Tanbir Ahmed <iamtanbirahmed@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented May 12, 2026

Copy link
Copy Markdown

Note

.coderabbit.yaml has unrecognized properties

CodeRabbit is using all valid settings from your configuration. Unrecognized properties (listed below) have been ignored and may indicate typos or deprecated fields that can be removed.

⚠️ Parsing warnings (1)
Validation error: Unrecognized key(s) in object: 'path_filters', 'path_instructions'
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • 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
📝 Walkthrough

Walkthrough

ResourceTable adds interactive multi-column sorting: Shift-aware header clicks update an ordered _sort_columns list; header labels show ▲/▼ plus rank; populate_resources preserves or resets sort state, caches rows, and _apply_filter filters then stably sorts visible rows before rendering.

Changes

Multi-column Sorting Implementation

Layer / File(s) Summary
Imports and event handlers
src/gantry/widgets.py
Adds Tuple and rich.text.Text imports. Implements on_mouse_down to record Shift state and on_data_table_header_selected to compute/update _sort_columns, toggle directions, handle Shift+click for secondary sorts, and update header labels with ▲/▼ and sort rank.
Label decorators and type coercion
src/gantry/widgets.py
Adds helpers to build sort-decorated header Text objects showing direction and rank, and type-aware coercion functions used by the sorter.
Column rebuild and row caching
src/gantry/widgets.py
Refactors populate_resources to produce stable column keys and labels (including indicator text), preserve _sort_columns only when the incoming columns list is unchanged, cache all row values in _all_rows, and defer actual row rendering to _apply_filter.
Stable multi-key sorter and filter integration
src/gantry/widgets.py
Adds _sort_items to perform stable multi-key ascending/descending sorts across _sort_columns. Modifies _apply_filter to always compute visible rows from the search term, call _sort_items, clear existing rows, and re-add rows in sorted order.
Test suite for sorting behavior
tests/test_app.py
New unit and integration tests validate initial sort state, _sort_items behavior (no-op when empty, single/multi-column ordering, case-insensitive compare), _compute_next_sort transitions (primary setup, toggles, shift additions, max three columns), and integration scenarios for sort state reset/preservation and sorted row rendering.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 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 'feat: multi-column sort for ResourceTable' accurately summarizes the main change: implementing multi-column sorting functionality for the ResourceTable widget.
Linked Issues check ✅ Passed The PR implementation satisfies all primary coding requirements from issue #49: clickable headers, ascending/descending toggle, Shift+Click for secondary/tertiary sorts (up to 3 columns), visual indicators (▲/▼) with rank numbers, and stable multi-column sorting logic.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing multi-column sort functionality for ResourceTable, including the widget implementation, supporting helper methods, test coverage, and state management—nothing appears out of scope.
Docstring Coverage ✅ Passed Docstring coverage is 96.55% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/issue-49-20260512-2214

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/gantry/widgets.py`:
- Around line 97-119: Duplicate label-building logic in _update_column_labels
and populate_resources should be extracted into a single helper (e.g.,
_build_column_label or _column_label_for_index) that accepts the column index
and base label and returns the final Text label; move computation of sort_map,
multi, indicator, and suffix into that helper and have both
_update_column_labels and populate_resources call it to set column.label (and
preserve calling self.refresh() in _update_column_labels). Update references to
self._sort_columns, self._columns, and the column objects inside the new helper
so it can be used from both locations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 671985b1-fdee-4ad6-be49-5a6ffe67ca15

📥 Commits

Reviewing files that changed from the base of the PR and between 5a66c3e and c9fff70.

📒 Files selected for processing (2)
  • src/gantry/widgets.py
  • tests/test_app.py

Comment thread src/gantry/widgets.py Outdated
@iamtanbirahmed

Copy link
Copy Markdown
Owner Author

@claude, address these comments

@claude

claude Bot commented May 12, 2026

Copy link
Copy Markdown

Claude finished @iamtanbirahmed's task in 1m 20s —— View job


Addressing CodeRabbit Review Comments

  • Analyze the review comments
  • Read src/gantry/widgets.py to understand current duplicate code
  • Extract _build_column_label_text helper method
  • Update _update_column_labels to use the helper
  • Update populate_resources to use the helper
  • Verify code correctness (syntax and logic review)
  • Commit and push changes

Addressed the CodeRabbit refactoring suggestion by extracting a _build_column_label_text(col_name, col_idx) helper method in src/gantry/widgets.py. The duplicated sort_map/multi/indicator/suffix logic that existed in both _update_column_labels and populate_resources is now consolidated in the single helper. Both methods call it — _update_column_labels wraps the result in Text(), while populate_resources passes it directly as a string to add_column.
|

…ate sort label logic

Co-authored-by: Tanbir Ahmed <iamtanbirahmed@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/gantry/widgets.py`:
- Line 155: populate_resources currently coerces all values to strings
(row_values = [str(resource.get(k, "")) for k in column_keys]) which destroys
numeric/date types and breaks _sort_items' comparison; instead preserve raw
typed values when building each row (keep a parallel raw_values list or store
values without str() in populate_resources for the same column_keys) and pass
those raw values into _sort_items; update _sort_items to perform type-aware
comparisons (handle numbers and datetimes first, then fallback to
case-insensitive string comparison, and handle None consistently) so sorting
respects numeric/date semantics while still using stringified display values for
rendering.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d1ee09be-c1b2-432c-85e0-05d4609cc2bd

📥 Commits

Reviewing files that changed from the base of the PR and between c9fff70 and 882f42a.

📒 Files selected for processing (1)
  • src/gantry/widgets.py

Comment thread src/gantry/widgets.py Outdated
@iamtanbirahmed

Copy link
Copy Markdown
Owner Author

@claude address these comments

@claude

claude Bot commented May 12, 2026

Copy link
Copy Markdown

Claude finished @iamtanbirahmed's task in 3m 28s —— View job


Addressing CodeRabbit Review Comments

  • Analyze review comments
  • Read src/gantry/widgets.py to understand current state
  • Add _coerce_sort_value helper for type-aware comparisons (int/float, datetime, string)
  • Update populate_resources to store raw (non-stringified) values in _all_rows
  • Update _sort_items to use _coerce_sort_value instead of str().lower()
  • Update add_row call to stringify values for display rendering
  • Commit and push changes (e6994f0)

What was fixed: CodeRabbit flagged that _sort_items used str(val).lower() for all comparisons, causing numeric columns (e.g., a "Restarts" column with values 10, 2, 9) to sort lexicographically — "10" before "2" before "9" — rather than by value.

Changes made in src/gantry/widgets.py:

  1. New _coerce_sort_value(value) method — returns a (type_tag, coerced_value) tuple:

    • (0, float) for numeric strings — sorts numerically
    • (1, datetime) for ISO-format datetime strings — sorts chronologically
    • (2, str.lower()) for everything else — sorts case-insensitively
    • The tuple structure ensures cross-type comparisons never raise TypeError
  2. populate_resources — removed str() coercion at row-storage time (row_values = [resource.get(k, "") for k in column_keys]); raw typed values are now preserved in _all_rows

  3. _sort_items — uses _coerce_sort_value instead of str(val).lower() as the sort key

  4. _apply_filter — stringifies only at render time: add_row(*[str(v) for v in row_values], ...)
    | Branch

…ting

Previously all resource values were stringified at storage time, causing
numeric columns (e.g., restarts: 10) to sort lexicographically ("10" < "2").

- Add _coerce_sort_value returning a (type_tag, value) tuple so numbers,
  ISO datetimes, and strings each sort correctly and cross-type comparisons
  never raise TypeError
- Store raw (un-stringified) values in _all_rows; stringify only at render
  time via add_row(*[str(v) for v in row_values])
- Update _sort_items to use _coerce_sort_value instead of str().lower()

Co-authored-by: Tanbir Ahmed <iamtanbirahmed@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/gantry/widgets.py`:
- Around line 55-57: The current shift state stored in self._shift_held (set in
on_mouse_down) can become stale or be missing if header selection happens
without a prior mouse event; update on_data_table_header_selected to defensively
read the event modifier (e.g., shift = getattr(event, "shift", False)) and then,
after handling the header selection logic that uses self._shift_held, reset
self._shift_held = False so subsequent interactions aren’t affected by stale
state; reference the methods on_mouse_down and on_data_table_header_selected and
the attribute _shift_held when making this change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 129d8640-6cbb-4385-95d6-86388794ee27

📥 Commits

Reviewing files that changed from the base of the PR and between 882f42a and e6994f0.

📒 Files selected for processing (1)
  • src/gantry/widgets.py

Comment thread src/gantry/widgets.py
@iamtanbirahmed

Copy link
Copy Markdown
Owner Author

@claude

@claude

claude Bot commented May 13, 2026

Copy link
Copy Markdown

Claude finished @iamtanbirahmed's task in 50s —— View job


Addressing CodeRabbit Review Comments

  • Analyze review comments
  • Read src/gantry/widgets.py to understand current shift state handling
  • Reset _shift_held = False at end of on_data_table_header_selected
  • Commit and push changes (c01d964)

What was fixed: CodeRabbit flagged that _shift_held (set by on_mouse_down) could become stale — if on_data_table_header_selected fires without a preceding mouse-down (e.g., keyboard navigation), _shift_held would retain its last value and incorrectly treat the click as a Shift+Click.

Change in src/gantry/widgets.py: Added self._shift_held = False immediately after consuming the shift state in on_data_table_header_selected (line 95), ensuring the flag is always reset after each header selection regardless of how the event was triggered.
| Branch

Ensures subsequent header clicks aren't incorrectly treated as Shift+Click
if on_mouse_down doesn't fire first (e.g., keyboard navigation).

Co-authored-by: Tanbir Ahmed <iamtanbirahmed@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/gantry/widgets.py`:
- Line 11: The module imports rich.text.Text at top-level (see import of Text in
src/gantry/widgets.py), but rich is not declared in pyproject.toml dependencies;
add "rich" (compatible version, e.g. same major as current transitively used by
textual) to the [project]/dependencies in pyproject.toml so the package declares
a direct dependency rather than relying on a transitive dependency; update the
version specifier to match your compatibility policy and run dependency tooling
(poetry/pip) to refresh lockfiles.
- Around line 70-77: The current shift-click branch appends a new sort tuple to
self._sort_columns and when len > 3 uses pop(0), which discards the primary
sort; change the overflow removal to drop the least-significant pre-existing key
instead of the primary by popping the second-to-last entry after the append
(e.g. replace self._sort_columns.pop(0) with self._sort_columns.pop(-2)) so
primary remains at index 0; adjust only the overflow branch in the shift
handling around existing_pos/col_idx/_sort_columns.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1e0fba24-2420-4fb0-896c-1fa24cf3b9fb

📥 Commits

Reviewing files that changed from the base of the PR and between e6994f0 and c01d964.

📒 Files selected for processing (1)
  • src/gantry/widgets.py

Comment thread src/gantry/widgets.py
from textual.message import Message
from textual.events import Key
from textual.css.query import NoMatches
from rich.text import Text

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm rich is not declared as a direct dependency in pyproject.toml
fd -t f 'pyproject.toml' --max-depth 3 --exec cat {}
echo "---"
fd -t f 'pyproject.toml' --max-depth 3 --exec rg -n '\brich\b' {} \;

Repository: iamtanbirahmed/gantry

Length of output: 468


Declare rich as a direct dependency.

rich.text.Text is imported and used at module level, but rich is not listed in dependencies within pyproject.toml. While rich is available transitively through textual, relying on this is fragile—a future Textual release could drop or replace it. Add rich to pyproject.toml's dependencies.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/gantry/widgets.py` at line 11, The module imports rich.text.Text at
top-level (see import of Text in src/gantry/widgets.py), but rich is not
declared in pyproject.toml dependencies; add "rich" (compatible version, e.g.
same major as current transitively used by textual) to the
[project]/dependencies in pyproject.toml so the package declares a direct
dependency rather than relying on a transitive dependency; update the version
specifier to match your compatibility policy and run dependency tooling
(poetry/pip) to refresh lockfiles.

Comment thread src/gantry/widgets.py
Comment on lines +70 to +77
if shift:
if existing_pos is not None:
idx, rev = self._sort_columns[existing_pos]
self._sort_columns[existing_pos] = (idx, not rev)
else:
self._sort_columns.append((col_idx, False))
if len(self._sort_columns) > 3:
self._sort_columns.pop(0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Shift+Click overflow silently demotes the primary sort column.

When _sort_columns already has 3 entries and the user shift-clicks a new column, pop(0) discards the primary sort key, promoting what was the secondary to primary. This is surprising UX — the user's most-significant sort silently disappears while they were trying to add a tertiary. Most multi-sort UIs either ignore the addition past the cap or drop the least significant key (last secondary), preserving the user's primary intent.

♻️ Proposed fix — drop the least-significant sort instead of the primary
         if shift:
             if existing_pos is not None:
                 idx, rev = self._sort_columns[existing_pos]
                 self._sort_columns[existing_pos] = (idx, not rev)
             else:
-                self._sort_columns.append((col_idx, False))
-                if len(self._sort_columns) > 3:
-                    self._sort_columns.pop(0)
+                if len(self._sort_columns) >= 3:
+                    # At cap: drop the least-significant key, keep primary intent.
+                    self._sort_columns.pop()
+                self._sort_columns.append((col_idx, False))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/gantry/widgets.py` around lines 70 - 77, The current shift-click branch
appends a new sort tuple to self._sort_columns and when len > 3 uses pop(0),
which discards the primary sort; change the overflow removal to drop the
least-significant pre-existing key instead of the primary by popping the
second-to-last entry after the append (e.g. replace self._sort_columns.pop(0)
with self._sort_columns.pop(-2)) so primary remains at index 0; adjust only the
overflow branch in the shift handling around existing_pos/col_idx/_sort_columns.

@iamtanbirahmed

Copy link
Copy Markdown
Owner Author

@claude

@claude

claude Bot commented May 13, 2026

Copy link
Copy Markdown

Claude finished @iamtanbirahmed's task in 6m 12s —— View jobclaude/pr-52-20260513-0148Create PR ➔


Addressing CodeRabbit Review Comments

The PR was closed without merging, so I implemented the full multi-column sort feature on a new branch with all four CodeRabbit fixes applied from the start.

What was implemented

src/gantry/widgets.py — full multi-column sort on ResourceTable:

  • Click column header → ascending sort; click again → descending
  • Shift+Click → add as secondary/tertiary sort (max 3 columns)
  • Sort indicators / with rank numbers for multi-sort (e.g. Name ▲1, Age ▼2)
  • Sort state preserved on refresh; resets only when column layout changes
  • All four CodeRabbit fixes applied (no intermediate bugs)

pyproject.tomlrich>=13.0.0 added as explicit dependency

tests/test_app.py — 20 new tests covering:

  • Initial sort state, _sort_items (no-op, single/multi-column, case-insensitive, numeric ordering)
  • _compute_next_sort transitions (primary setup, toggle, Shift add, max-3 overflow preserves primary)
  • _build_column_label_text (no sort, ascending, descending, multi-sort with rank)
  • Sort state reset/preservation on populate_resources
  • _shift_held reset after header selection

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.

Multi-column Sort

1 participant