Skip to content

feat: Enhanced file filtering and grouping by file paths#265

Merged
JamesGeorg merged 2 commits intomainfrom
file-filtering-grouping
Mar 9, 2026
Merged

feat: Enhanced file filtering and grouping by file paths#265
JamesGeorg merged 2 commits intomainfrom
file-filtering-grouping

Conversation

@yuvrajjsingh0
Copy link
Copy Markdown
Contributor

@yuvrajjsingh0 yuvrajjsingh0 commented Feb 27, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • File grouping: Files page now displays files organized into expandable groups showing versions and metadata
    • Tag-based filtering: Filter files by tags using a new dropdown selector
    • Enhanced search: Debounced search functionality for faster file discovery
    • Improved file selection: Redesigned workflow for selecting files when creating packages
  • Improvements

    • Fixed bottom action bar: Navigation controls now remain visible while scrolling
    • Better pagination: Updated pagination controls for browsing file groups

@semanticdiff-com
Copy link
Copy Markdown

semanticdiff-com Bot commented Feb 27, 2026

Review changes with  SemanticDiff

Changed Files
File Status
  airborne_dashboard/components/release/ReleaseBuilder.tsx  89% smaller
  airborne_dashboard/components/release/steps/FilePrioritiesStep.tsx  28% smaller
  airborne_dashboard/app/dashboard/[orgId]/[appId]/files/page.tsx  25% smaller
  airborne_dashboard/app/dashboard/[orgId]/[appId]/packages/create/page.tsx  16% smaller
  airborne_server/src/file.rs  15% smaller
  airborne_dashboard/components/release/steps/ResourcesStep.tsx  5% smaller
  airborne_server_clients/javascript/sdk/dist-es/protocols/Aws_restJson1.js  4% smaller
  airborne_server_clients/javascript/sdk/dist-cjs/protocols/Aws_restJson1.js  4% smaller
  airborne_server_clients/javascript/sdk/src/protocols/Aws_restJson1.ts  3% smaller
  airborne_server_clients/javascript/sdk/dist-types/AirborneClient.d.ts  1% smaller
  airborne-core-cli/index.js  1% smaller
  airborne-core-cli/action.js  1% smaller
  airborne_dashboard/components/file-chooser.tsx  0% smaller
  airborne_dashboard/components/filter-dropdown.tsx  0% smaller
  airborne_dashboard/types/files.ts  0% smaller
  airborne_server/src/file/groups.rs  0% smaller
  airborne_server/src/file/groups/types.rs  0% smaller
  airborne_server/src/file/types.rs  0% smaller
  airborne_server/src/types.rs  0% smaller
  airborne_server_clients/javascript/sdk/dist-cjs/Airborne.js  0% smaller
  airborne_server_clients/javascript/sdk/dist-cjs/commands/ListFileGroupsCommand.js  0% smaller
  airborne_server_clients/javascript/sdk/dist-cjs/commands/index.js  0% smaller
  airborne_server_clients/javascript/sdk/dist-es/Airborne.js  0% smaller
  airborne_server_clients/javascript/sdk/dist-es/commands/ListFileGroupsCommand.js  0% smaller
  airborne_server_clients/javascript/sdk/dist-es/commands/index.js  0% smaller
  airborne_server_clients/javascript/sdk/dist-types/Airborne.d.ts  0% smaller
  airborne_server_clients/javascript/sdk/dist-types/commands/ListFileGroupsCommand.d.ts Unsupported file format
  airborne_server_clients/javascript/sdk/dist-types/commands/ListFilesCommand.d.ts Unsupported file format
  airborne_server_clients/javascript/sdk/dist-types/commands/index.d.ts  0% smaller
  airborne_server_clients/javascript/sdk/dist-types/models/models_0.d.ts  0% smaller
  airborne_server_clients/javascript/sdk/dist-types/protocols/Aws_restJson1.d.ts  0% smaller
  airborne_server_clients/javascript/sdk/package.json  0% smaller
  airborne_server_clients/javascript/sdk/src/Airborne.ts  0% smaller
  airborne_server_clients/javascript/sdk/src/AirborneClient.ts  0% smaller
  airborne_server_clients/javascript/sdk/src/commands/ListFileGroupsCommand.ts  0% smaller
  airborne_server_clients/javascript/sdk/src/commands/ListFilesCommand.ts  0% smaller
  airborne_server_clients/javascript/sdk/src/commands/index.ts  0% smaller
  airborne_server_clients/javascript/sdk/src/models/models_0.ts  0% smaller
  airborne_server_clients/javascript/sdk/tsconfig.cjs.tsbuildinfo Unsupported file format
  airborne_server_clients/javascript/sdk/tsconfig.es.tsbuildinfo Unsupported file format
  airborne_server_clients/javascript/sdk/tsconfig.types.tsbuildinfo Unsupported file format
  airborne_server_clients/model/model.json  0% smaller
  smithy/models/file.smithy Unsupported file format
  smithy/models/main.smithy Unsupported file format

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 27, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

This PR introduces a grouped, paginated file management system across the dashboard and backend. It adds a new FileChooser component for browsing and selecting files with tag filtering and search, integrates this component into package creation and release builder workflows, adds new backend endpoints for file groups and tags, and updates the files page to display grouped file views.

Changes

Cohort / File(s) Summary
Files Page & Package Creation UI
airborne_dashboard/app/dashboard/.../files/page.tsx, airborne_dashboard/app/dashboard/.../packages/create/page.tsx
Replaced flat file lists with FileChooser-based selection, removed SWR usage and pagination logic, added tag-based filtering and improved UI structure with fixed bottom action bars.
Resource Selection in Release Builder
airborne_dashboard/components/release/ResourcesStep.tsx
Replaced server-driven resource listing with client-side FileChooser component, removed search/pagination APIs, added logic to exclude existing package files from selection.
File Chooser Component
airborne_dashboard/components/file-chooser.tsx
New reusable component for browsing, filtering, and selecting files with single/multi-select modes, debounced search, tag filtering, pagination, and version-level selection controls.
UI & Layout Components
airborne_dashboard/components/filter-dropdown.tsx, airborne_dashboard/components/release/ReleaseBuilder.tsx
New FilterDropdown component for searchable, multi-select dropdown with infinite scroll support; updated ReleaseBuilder with fixed bottom action bar and spacer adjustments.
Frontend Type Definitions
airborne_dashboard/types/files.ts
New shared TypeScript types for FileGroup, FileGroupVersion, FileGroupTag, SelectedFile, query parameters, and API responses supporting grouped file listing.
Backend File Grouping Endpoints
airborne_server/src/file/groups.rs, airborne_server/src/file/groups/types.rs
New Rust module providing grouped file listing with filtering, searching, pagination; includes FileGroupsQuery, FileGroupVersion, FileGroupResponse, and FileGroupsListResponse types.
Backend File & Tag APIs
airborne_server/src/file.rs, airborne_server/src/file/types.rs, airborne_server/src/types.rs
Added tags filtering to list_files, new list_file_tags endpoint with aggregation logic, added FileTagInfo type, enhanced PaginatedQuery with Clone derivation and custom deserialization.
API Schema Definitions
smithy/models/file.smithy
Added tags parameter to ListFilesRequest, introduced ListFileTagsRequest/Response structures and ListFileTags operation for tag enumeration.

Sequence Diagram(s)

sequenceDiagram
    actor User as User
    participant UI as FileChooser UI
    participant API as API Client
    participant Server as Backend Server
    participant DB as Database

    User->>UI: Enter search term or select tag
    UI->>API: Debounce & fetch groups (search + tag filters)
    API->>Server: GET /file/groups?search=...&tags=...&page=1
    Server->>DB: Query file groups with tag/search filters
    DB-->>Server: Return matching groups + pagination metadata
    Server-->>API: FileGroupsResponse {groups, total_items, total_pages}
    API-->>UI: Render groups, versions, tags
    
    User->>UI: Expand file group & select version
    UI->>UI: Toggle version in selected files
    UI->>UI: Convert SelectedFile[] to resource IDs
    
    User->>UI: Click confirm/create
    UI->>API: POST /packages or /releases with selected file IDs
    API->>Server: Create package/release with resource references
    Server-->>API: Success response
    API-->>User: Confirm & navigate
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • PR #236: Modifies ReleaseBuilder and ResourcesStep components with file-selection integration; overlaps with this PR's changes to those same components and the new FileChooser/tag-filtering architecture.

Poem

🐰 Files now grouped in tidy rows,
Tags and versions, search that flows,
FileChooser hops through every page,
Organization's the modern stage! 🌟

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: introducing enhanced file filtering and grouping capabilities organized by file paths, which is the primary focus across the modified file listing, file chooser, and API endpoints.

✏️ 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 file-filtering-grouping

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.

@yuvrajjsingh0 yuvrajjsingh0 added enhancement New feature or request javascript Pull requests that update javascript code rust Pull requests that update rust code labels Feb 27, 2026
@yuvrajjsingh0 yuvrajjsingh0 self-assigned this Feb 27, 2026
Copy link
Copy Markdown

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

🧹 Nitpick comments (14)
airborne_dashboard/app/dashboard/[orgId]/[appId]/files/page.tsx (1)

68-73: Hardcoded limit of 100 tags may truncate available options.

The tags endpoint fetches with count: 100, which could miss tags if an application has more than 100 unique tags. Consider fetching all tags (all: true) or implementing a searchable tag selector for larger datasets.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@airborne_dashboard/app/dashboard/`[orgId]/[appId]/files/page.tsx around lines
68 - 73, The current useSWR call that populates tagsData uses
apiFetch("/file/tags", ...) with a hardcoded query of { page: 1, count: 100 }
which can truncate results; update the query passed to apiFetch in the useSWR
block (the call inside useSWR that returns apiFetch<TagsResponse>("/file/tags",
...)) to request all tags (e.g., replace count: 100 with all: true or add all:
true depending on backend support) or implement a paginated/searchable approach
(e.g., switch to server-side search or incremental loading in the tag selector)
so the "/file/tags" endpoint returns the full set instead of only the first 100;
ensure the downstream handling of tagsData in the component accommodates the
full result shape.
airborne_server/src/types.rs (1)

370-377: Consider handling empty string query parameters gracefully.

If a query parameter is present but empty (e.g., ?page=), this will deserialize as Some("") and parse() will fail with an error. Depending on desired behavior, you may want to treat empty strings as None:

♻️ Optional: treat empty strings as absent
 fn de_u32_from_str<'de, D>(d: D) -> std::result::Result<Option<u32>, D::Error>
 where
     D: Deserializer<'de>,
 {
     let opt: Option<String> = Option::deserialize(d)?;
-    opt.map(|s| s.parse().map_err(serde::de::Error::custom))
+    opt.filter(|s| !s.is_empty())
+        .map(|s| s.parse().map_err(serde::de::Error::custom))
         .transpose()
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@airborne_server/src/types.rs` around lines 370 - 377, The deserializer
de_u32_from_str currently treats an empty query value (Some("")) as a parse
error; update de_u32_from_str so that when Option<String> is Some(s) and
s.trim().is_empty() it returns Ok(None) instead of attempting to parse,
otherwise parse the string to u32 and propagate parse errors via
serde::de::Error::custom; refer to the function name de_u32_from_str and the
existing Option<String> handling to locate where to add the empty-string check
and branch.
airborne_server/src/file/groups.rs (1)

128-175: N+1 query pattern may cause performance issues at scale.

For each file_path in the page, two additional queries are executed (fetch versions + count total). With a page of 15 items, this results in 30+ queries per request. Consider batching these queries or using a single query with window functions for better performance as data grows.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@airborne_server/src/file/groups.rs` around lines 128 - 175, The loop over
file_paths causes an N+1 query pattern: for each fp you run two queries (the
file_versions query and total_versions count) which scales poorly; change to
batch queries by (1) fetching all relevant versions for the page in one query
(filter
files.filter(org_id.eq(&organisation)).filter(app_id.eq(&application)).filter(file_path.eq_any(&file_paths)).order(file_path.asc()).order(version.desc()).select(DbFile::as_select()).load(&mut
conn)?) and grouping results in-memory by file_path to build the versions and
tags vectors for each FileGroupResponse, and (2) getting total_versions for all
file_paths in one grouped count query
(files.filter(...).filter(file_path.eq_any(&file_paths)).select((file_path,
diesel::dsl::count_star())).group_by(file_path).load(...)?) then map counts back
to each fp; update the code that currently uses file_versions, total_versions
and the for fp in file_paths loop to use these batched results.
airborne_dashboard/components/release/ReleaseBuilder.tsx (1)

133-133: Hardcoded sidebar offset may break if layout changes.

The left-64 assumes a fixed sidebar width of 256px. If the sidebar width changes (e.g., for responsive design or a collapsed state), this bar will misalign. Consider using a CSS variable or a layout context to synchronize the offset.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@airborne_dashboard/components/release/ReleaseBuilder.tsx` at line 133, The
fixed left-64 on the bottom bar in ReleaseBuilder.tsx will misalign if the
sidebar width changes; replace the hardcoded class on the <div> (the element
with className="fixed bottom-0 left-64 right-0 bg-background border-t p-4 z-50")
with a responsive offset driven by a CSS variable or layout context: remove
left-64 and apply a left style like left: var(--sidebar-width) (or compute using
a sidebarWidth prop from your layout context and set style={{ left: sidebarWidth
}}), and ensure the sidebar component sets --sidebar-width (e.g.,
--sidebar-width: 16rem or toggles for collapsed state) so the bar always aligns
with the sidebar across breakpoints and collapsed/expanded states.
airborne_dashboard/components/file-chooser.tsx (3)

307-323: Redundant condition for totalPages > 1.

The check totalPages > 1 on Line 307 is always true in this branch because we're inside the else block where totalPages > maxVisible (5). This is harmless but could be simplified.

♻️ Simplified condition
-      if (totalPages > 1) {
-        items.push(
+      items.push(

Then remove the corresponding closing brace.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@airborne_dashboard/components/file-chooser.tsx` around lines 307 - 323, The
extra check "if (totalPages > 1)" around the final PaginationItem is redundant
because this branch already only runs when totalPages > maxVisible (5); remove
that outer conditional and its matching closing brace so the PaginationItem
block (using PaginationItem, PaginationLink, currentPage and setCurrentPage) is
unconditionally rendered in this branch, keeping the inner onClick, isActive and
key logic unchanged.

117-129: Consider using a more stable dependency for tag accumulation.

The effect uses tagsData?.data as a dependency, which may trigger re-runs when SWR returns a new array reference even if the content is the same. For this use case it's likely fine since SWR caches responses, but you could consider using JSON.stringify(tagsData?.data) or a custom comparison if you observe unnecessary re-renders.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@airborne_dashboard/components/file-chooser.tsx` around lines 117 - 129, The
useEffect depends on tagsData?.data which can change reference without content
changes; update the dependency to a stable representation (e.g.,
JSON.stringify(tagsData?.data) or a derived key) so the effect only re-runs when
actual tag content changes; modify the useEffect dependency array (and keep
logic inside useEffect using tagPage, tagsData?.data and setAccumulatedTags) to
use that stable dependency instead of raw tagsData?.data to prevent unnecessary
accumulations in the handlers that update accumulated tags.

412-419: Add aria-label to remove button for accessibility.

The remove button inside the tag badge lacks an accessible label, which makes it unclear to screen reader users what the button does.

♿ Add aria-label
             <button
               onClick={() => handleTagFilterToggle(tag)}
               className="hover:bg-muted rounded-sm p-0.5"
               disabled={disabled}
+              aria-label={`Remove ${tag} filter`}
             >
               <X className="h-3 w-3" />
             </button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@airborne_dashboard/components/file-chooser.tsx` around lines 412 - 419, The
remove button inside the Badge lacks an accessible label; update the button in
the Badge where onClick calls handleTagFilterToggle(tag) to include an
aria-label describing its action (e.g., "Remove tag {tag}" or "Remove filter
{tag}") so screen readers can convey purpose; keep the existing onClick,
className, and disabled props and ensure the label incorporates the tag variable
to be unique and descriptive for each badge.
airborne_server/src/file.rs (2)

590-594: Redundant null filtering in tag results.

The filter_map on Line 591-593 filters out None tags, but the base query already includes .filter(tag.is_not_null()) (Line 551). While this defensive coding is harmless, it's technically unnecessary given the query constraints.

♻️ Simplified mapping (optional)
-        let tags: Vec<FileTagInfo> = tag_results
-            .into_iter()
-            .filter_map(|(tag_opt, count)| tag_opt.map(|t| FileTagInfo { tag: t, count }))
-            .collect();
+        let tags: Vec<FileTagInfo> = tag_results
+            .into_iter()
+            .map(|(tag_opt, count)| FileTagInfo { 
+                tag: tag_opt.expect("base query filters nulls"), 
+                count 
+            })
+            .collect();

Note: Keeping filter_map is safer if query logic changes later.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@airborne_server/src/file.rs` around lines 590 - 594, The mapping from query
results redundantly filters out None values even though the DB query already
applies .filter(tag.is_not_null()); remove the unnecessary filter_map by
directly mapping tag_results into Vec<FileTagInfo> (use
tag_results.into_iter().map(|(tag, count)| FileTagInfo { tag, count
}).collect()) to simplify code around the tag_results handling and construction
of FileTagInfo while keeping the same behavior; update references in this block
where FileTagInfo is produced to use the direct map and ensure types still
match.

444-494: Code duplication between count and data queries.

The search and tag filtering logic is duplicated between the count query (Lines 446-466) and the data query (Lines 471-494). While this is somewhat unavoidable with Diesel's boxed query pattern, consider extracting a helper macro or function to build the filter conditions, which would improve maintainability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@airborne_server/src/file.rs` around lines 444 - 494, The count_query and
data_query blocks duplicate the same search/tag filter construction (using
files, org_id, app_id, search_term -> file_path.ilike/url.ilike, and tags_filter
-> tag.eq/tag.eq_any) before executing count() or load::<DbFile>(); refactor by
extracting a helper that accepts the base boxed query (e.g.,
files.into_boxed()), the optional &search_term, and &tags_filter and returns the
boxed query with filters applied, then call that helper to produce the query for
both total (count_query) and file_list (data_query) to eliminate duplication
while keeping Diesel types consistent.
airborne_dashboard/app/dashboard/[orgId]/[appId]/packages/create/page.tsx (2)

186-192: Hardcoded light-mode colors won't work in dark mode.

The selected index file indicator uses bg-green-50 and border-green-200 which are light-mode specific. Consider using theme-aware colors or adding dark mode variants.

🎨 Add dark mode support
-                  <div className="mt-4 p-3 bg-green-50 border border-green-200 rounded-lg">
+                  <div className="mt-4 p-3 bg-green-50 dark:bg-green-950 border border-green-200 dark:border-green-800 rounded-lg">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@airborne_dashboard/app/dashboard/`[orgId]/[appId]/packages/create/page.tsx
around lines 186 - 192, The selected index file card uses hardcoded light-mode
classes (bg-green-50, border-green-200, text-green-600) which will look wrong in
dark mode; update the JSX for the card around selectedIndexFile (the div with
classes including bg-green-50 and border-green-200 and the FileText span using
text-green-600) to use Tailwind dark variants (for example: bg-green-50
dark:bg-green-900, border-green-200 dark:border-green-700, text-green-600
dark:text-green-300) or replace with theme-aware utility tokens so the
background, border and icon/text colors change appropriately in dark mode while
preserving accessibility.

220-221: Hardcoded sidebar offset may cause layout issues.

The fixed bottom bar uses left-64 which assumes a sidebar width of 16rem (256px). If the sidebar width changes or is collapsible, this will cause misalignment. Consider using a CSS custom property or layout context to maintain consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@airborne_dashboard/app/dashboard/`[orgId]/[appId]/packages/create/page.tsx
around lines 220 - 221, The fixed bottom bar currently hardcodes the sidebar
offset via the class "left-64" in the JSX element with className "fixed bottom-0
left-64 right-0 bg-background border-t p-4 z-50"; replace that hardcoded offset
with a responsive approach such as using a CSS custom property (e.g. left:
var(--sidebar-width)) or reading the sidebar width from a shared layout
context/prop so the bottom bar aligns when the sidebar changes/collapses, and
ensure the corresponding CSS/root style or context provider sets/updates that
--sidebar-width value (or prop) used by the bottom bar.
airborne_dashboard/components/filter-dropdown.tsx (3)

76-82: Missing cleanup when onSearch changes during debounce.

The debounce effect includes onSearch in the dependency array. If onSearch is not memoized by the parent component, this could cause the timeout to be reset on every render, potentially delaying or preventing the search callback from firing.

♻️ Use ref to avoid dependency on onSearch
+  const onSearchRef = useRef(onSearch);
+  onSearchRef.current = onSearch;
+
   // Handle search with debounce
   useEffect(() => {
     const timer = setTimeout(() => {
-      onSearch?.(searchQuery);
+      onSearchRef.current?.(searchQuery);
     }, 300);
     return () => clearTimeout(timer);
-  }, [searchQuery, onSearch]);
+  }, [searchQuery]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@airborne_dashboard/components/filter-dropdown.tsx` around lines 76 - 82, The
debounce effect currently depends on onSearch which can change each render and
reset the timer; change to store the callback in a ref (e.g., onSearchRef) and
update that ref when onSearch changes, then have the debounce useEffect
reference only searchQuery (not onSearch) so the timeout is not restarted on
every parent render; keep the existing timer creation/clearTimeout cleanup in
the effect that triggers on searchQuery, and invoke
onSearchRef.current(searchQuery) inside the timeout to call the latest callback.

163-179: Missing keyboard navigation and ARIA attributes for options.

The option items use div elements with onClick but lack keyboard navigation support (arrow keys, Enter to select) and proper ARIA attributes (role="option", aria-selected). This impacts accessibility for keyboard and screen reader users.

♿ Add accessibility attributes
                       <div
                         key={`${id}-${option.value}`}
                         onClick={() => toggleValue(option.value)}
+                        onKeyDown={(e) => {
+                          if (e.key === 'Enter' || e.key === ' ') {
+                            e.preventDefault();
+                            toggleValue(option.value);
+                          }
+                        }}
+                        role="option"
+                        aria-selected={isSelected}
+                        tabIndex={0}
                         className={cn(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@airborne_dashboard/components/filter-dropdown.tsx` around lines 163 - 179,
The option elements lack keyboard and ARIA support; update the option container
(the element keyed by `${id}-${option.value}` used in the map that calls
toggleValue) to include role="option", aria-selected={isSelected}, and
tabIndex={0}, and add an onKeyDown handler that triggers
toggleValue(option.value) on Enter or Space and implements ArrowUp/ArrowDown
focus movement (e.g., focus previousSibling/nextSibling or delegate to a
provided focus management callback) so keyboard users can navigate and select
options; ensure Check and showCounts usage remains unchanged.

84-92: Potential for multiple onLoadMore calls before loading state updates.

The scroll handler checks !isLoading before calling onLoadMore, but if the parent doesn't immediately update isLoading to true, rapid scroll events could trigger multiple calls. Consider adding local state or a ref to track if a load is already in progress.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@airborne_dashboard/components/filter-dropdown.tsx` around lines 84 - 92, The
scroll handler can call onLoadMore multiple times before the parent flips
isLoading; add a local ref (e.g., loadingRef) to guard re-entrancy in
handleScroll: check loadingRef.current || !hasMore || isLoading before calling,
set loadingRef.current = true immediately before invoking onLoadMore, then clear
it after the load finishes (if onLoadMore returns a Promise, await it and clear
in finally; otherwise clear when you get a callback/prop update). Update
handleScroll to reference loadingRef and ensure the ref is created via
useRef(false) so rapid scroll events cannot trigger duplicate onLoadMore calls.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@airborne_dashboard/app/dashboard/`[orgId]/[appId]/files/page.tsx:
- Around line 311-391: The mapping fragment currently lacks the key (it's placed
on the inner TableRow), causing React's missing key warning; move the key to the
outer fragment by replacing the shorthand <>...</> with a keyed fragment like
<React.Fragment key={group.file_path}> (or import { Fragment } from 'react' and
use <Fragment key={group.file_path}>), then remove the key prop from the inner
TableRow (the elements referenced are the fragment around the group block and
the TableRow that currently has key={group.file_path}).

In `@airborne_dashboard/app/dashboard/`[orgId]/[appId]/packages/create/page.tsx:
- Around line 34-38: The useEffect around access-checking (useEffect in
page.tsx) currently only depends on loadingAccess but references getOrgAccess,
getAppAccess, org, app, and hasAppAccess, risking stale closures; update the
dependency array to include getOrgAccess, getAppAccess, org, app, hasAppAccess
(or instead memoize those values/results before the effect) so the effect
re-runs whenever any of those change and still calls notFound() appropriately.

In `@airborne_dashboard/components/release/steps/ResourcesStep.tsx`:
- Around line 14-23: The mapping for selectedFiles in the useMemo uses `version
|| 1` which incorrectly treats version 0 as falsy; update the mapping that
builds each SelectedFile (inside the Array.from(selectedResources).map callback
where parseFileRef is used) to use nullish coalescing so a valid 0 is preserved
(i.e., use version ?? 1 instead of version || 1) so that SelectedFile.file_path,
version, and url are produced correctly.

In `@airborne_server/src/file/groups.rs`:
- Around line 63-66: The search term is being interpolated into the LIKE pattern
without escaping SQL wildcard characters, so escape '%' and '_' (and the escape
char itself) on the input before building the pattern used with file_path.ilike;
e.g., replace '\' with '\\', '%' with '\%', and '_' with '\_' (or another
consistent escape char), then build pattern = format!("%{}%", escaped_search)
and pass that to count_query.filter(file_path.ilike(pattern).escape('\\')) (or
use your ORM's ESCAPE mechanism) so the literal characters are matched; apply
the same escaping wherever search_term is used in other queries (e.g., any other
`.ilike(pattern)` calls).

---

Nitpick comments:
In `@airborne_dashboard/app/dashboard/`[orgId]/[appId]/files/page.tsx:
- Around line 68-73: The current useSWR call that populates tagsData uses
apiFetch("/file/tags", ...) with a hardcoded query of { page: 1, count: 100 }
which can truncate results; update the query passed to apiFetch in the useSWR
block (the call inside useSWR that returns apiFetch<TagsResponse>("/file/tags",
...)) to request all tags (e.g., replace count: 100 with all: true or add all:
true depending on backend support) or implement a paginated/searchable approach
(e.g., switch to server-side search or incremental loading in the tag selector)
so the "/file/tags" endpoint returns the full set instead of only the first 100;
ensure the downstream handling of tagsData in the component accommodates the
full result shape.

In `@airborne_dashboard/app/dashboard/`[orgId]/[appId]/packages/create/page.tsx:
- Around line 186-192: The selected index file card uses hardcoded light-mode
classes (bg-green-50, border-green-200, text-green-600) which will look wrong in
dark mode; update the JSX for the card around selectedIndexFile (the div with
classes including bg-green-50 and border-green-200 and the FileText span using
text-green-600) to use Tailwind dark variants (for example: bg-green-50
dark:bg-green-900, border-green-200 dark:border-green-700, text-green-600
dark:text-green-300) or replace with theme-aware utility tokens so the
background, border and icon/text colors change appropriately in dark mode while
preserving accessibility.
- Around line 220-221: The fixed bottom bar currently hardcodes the sidebar
offset via the class "left-64" in the JSX element with className "fixed bottom-0
left-64 right-0 bg-background border-t p-4 z-50"; replace that hardcoded offset
with a responsive approach such as using a CSS custom property (e.g. left:
var(--sidebar-width)) or reading the sidebar width from a shared layout
context/prop so the bottom bar aligns when the sidebar changes/collapses, and
ensure the corresponding CSS/root style or context provider sets/updates that
--sidebar-width value (or prop) used by the bottom bar.

In `@airborne_dashboard/components/file-chooser.tsx`:
- Around line 307-323: The extra check "if (totalPages > 1)" around the final
PaginationItem is redundant because this branch already only runs when
totalPages > maxVisible (5); remove that outer conditional and its matching
closing brace so the PaginationItem block (using PaginationItem, PaginationLink,
currentPage and setCurrentPage) is unconditionally rendered in this branch,
keeping the inner onClick, isActive and key logic unchanged.
- Around line 117-129: The useEffect depends on tagsData?.data which can change
reference without content changes; update the dependency to a stable
representation (e.g., JSON.stringify(tagsData?.data) or a derived key) so the
effect only re-runs when actual tag content changes; modify the useEffect
dependency array (and keep logic inside useEffect using tagPage, tagsData?.data
and setAccumulatedTags) to use that stable dependency instead of raw
tagsData?.data to prevent unnecessary accumulations in the handlers that update
accumulated tags.
- Around line 412-419: The remove button inside the Badge lacks an accessible
label; update the button in the Badge where onClick calls
handleTagFilterToggle(tag) to include an aria-label describing its action (e.g.,
"Remove tag {tag}" or "Remove filter {tag}") so screen readers can convey
purpose; keep the existing onClick, className, and disabled props and ensure the
label incorporates the tag variable to be unique and descriptive for each badge.

In `@airborne_dashboard/components/filter-dropdown.tsx`:
- Around line 76-82: The debounce effect currently depends on onSearch which can
change each render and reset the timer; change to store the callback in a ref
(e.g., onSearchRef) and update that ref when onSearch changes, then have the
debounce useEffect reference only searchQuery (not onSearch) so the timeout is
not restarted on every parent render; keep the existing timer
creation/clearTimeout cleanup in the effect that triggers on searchQuery, and
invoke onSearchRef.current(searchQuery) inside the timeout to call the latest
callback.
- Around line 163-179: The option elements lack keyboard and ARIA support;
update the option container (the element keyed by `${id}-${option.value}` used
in the map that calls toggleValue) to include role="option",
aria-selected={isSelected}, and tabIndex={0}, and add an onKeyDown handler that
triggers toggleValue(option.value) on Enter or Space and implements
ArrowUp/ArrowDown focus movement (e.g., focus previousSibling/nextSibling or
delegate to a provided focus management callback) so keyboard users can navigate
and select options; ensure Check and showCounts usage remains unchanged.
- Around line 84-92: The scroll handler can call onLoadMore multiple times
before the parent flips isLoading; add a local ref (e.g., loadingRef) to guard
re-entrancy in handleScroll: check loadingRef.current || !hasMore || isLoading
before calling, set loadingRef.current = true immediately before invoking
onLoadMore, then clear it after the load finishes (if onLoadMore returns a
Promise, await it and clear in finally; otherwise clear when you get a
callback/prop update). Update handleScroll to reference loadingRef and ensure
the ref is created via useRef(false) so rapid scroll events cannot trigger
duplicate onLoadMore calls.

In `@airborne_dashboard/components/release/ReleaseBuilder.tsx`:
- Line 133: The fixed left-64 on the bottom bar in ReleaseBuilder.tsx will
misalign if the sidebar width changes; replace the hardcoded class on the <div>
(the element with className="fixed bottom-0 left-64 right-0 bg-background
border-t p-4 z-50") with a responsive offset driven by a CSS variable or layout
context: remove left-64 and apply a left style like left: var(--sidebar-width)
(or compute using a sidebarWidth prop from your layout context and set style={{
left: sidebarWidth }}), and ensure the sidebar component sets --sidebar-width
(e.g., --sidebar-width: 16rem or toggles for collapsed state) so the bar always
aligns with the sidebar across breakpoints and collapsed/expanded states.

In `@airborne_server/src/file.rs`:
- Around line 590-594: The mapping from query results redundantly filters out
None values even though the DB query already applies .filter(tag.is_not_null());
remove the unnecessary filter_map by directly mapping tag_results into
Vec<FileTagInfo> (use tag_results.into_iter().map(|(tag, count)| FileTagInfo {
tag, count }).collect()) to simplify code around the tag_results handling and
construction of FileTagInfo while keeping the same behavior; update references
in this block where FileTagInfo is produced to use the direct map and ensure
types still match.
- Around line 444-494: The count_query and data_query blocks duplicate the same
search/tag filter construction (using files, org_id, app_id, search_term ->
file_path.ilike/url.ilike, and tags_filter -> tag.eq/tag.eq_any) before
executing count() or load::<DbFile>(); refactor by extracting a helper that
accepts the base boxed query (e.g., files.into_boxed()), the optional
&search_term, and &tags_filter and returns the boxed query with filters applied,
then call that helper to produce the query for both total (count_query) and
file_list (data_query) to eliminate duplication while keeping Diesel types
consistent.

In `@airborne_server/src/file/groups.rs`:
- Around line 128-175: The loop over file_paths causes an N+1 query pattern: for
each fp you run two queries (the file_versions query and total_versions count)
which scales poorly; change to batch queries by (1) fetching all relevant
versions for the page in one query (filter
files.filter(org_id.eq(&organisation)).filter(app_id.eq(&application)).filter(file_path.eq_any(&file_paths)).order(file_path.asc()).order(version.desc()).select(DbFile::as_select()).load(&mut
conn)?) and grouping results in-memory by file_path to build the versions and
tags vectors for each FileGroupResponse, and (2) getting total_versions for all
file_paths in one grouped count query
(files.filter(...).filter(file_path.eq_any(&file_paths)).select((file_path,
diesel::dsl::count_star())).group_by(file_path).load(...)?) then map counts back
to each fp; update the code that currently uses file_versions, total_versions
and the for fp in file_paths loop to use these batched results.

In `@airborne_server/src/types.rs`:
- Around line 370-377: The deserializer de_u32_from_str currently treats an
empty query value (Some("")) as a parse error; update de_u32_from_str so that
when Option<String> is Some(s) and s.trim().is_empty() it returns Ok(None)
instead of attempting to parse, otherwise parse the string to u32 and propagate
parse errors via serde::de::Error::custom; refer to the function name
de_u32_from_str and the existing Option<String> handling to locate where to add
the empty-string check and branch.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3c27527 and e8b5314.

📒 Files selected for processing (13)
  • airborne_dashboard/app/dashboard/[orgId]/[appId]/files/page.tsx
  • airborne_dashboard/app/dashboard/[orgId]/[appId]/packages/create/page.tsx
  • airborne_dashboard/components/file-chooser.tsx
  • airborne_dashboard/components/filter-dropdown.tsx
  • airborne_dashboard/components/release/ReleaseBuilder.tsx
  • airborne_dashboard/components/release/steps/ResourcesStep.tsx
  • airborne_dashboard/types/files.ts
  • airborne_server/src/file.rs
  • airborne_server/src/file/groups.rs
  • airborne_server/src/file/groups/types.rs
  • airborne_server/src/file/types.rs
  • airborne_server/src/types.rs
  • smithy/models/file.smithy

Comment thread airborne_dashboard/app/dashboard/[orgId]/[appId]/files/page.tsx Outdated
Comment thread airborne_dashboard/app/dashboard/[orgId]/[appId]/packages/create/page.tsx Outdated
Comment thread airborne_dashboard/components/release/steps/ResourcesStep.tsx
Comment thread airborne_server/src/file/groups.rs
@yuvrajjsingh0 yuvrajjsingh0 force-pushed the file-filtering-grouping branch from a444df7 to 014642e Compare March 2, 2026 10:56
@yuvrajjsingh0 yuvrajjsingh0 force-pushed the file-filtering-grouping branch from 3d78f84 to d0f090f Compare March 2, 2026 12:24
@JamesGeorg JamesGeorg merged commit 7a9f6ff into main Mar 9, 2026
14 of 15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request javascript Pull requests that update javascript code rust Pull requests that update rust code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants