Skip to content

Conversation

@timothyfroehlich
Copy link
Owner

Overview

This PR implements the comprehensive issue search and filtering system as designed in docs/plans/2026-01-12-issue-filter-search-design.md. It replaces the temporary mockup controls with a fully functional, production-ready implementation.

Key Features

  • Search & Filtering:
    • Free-text search across issue title, ID, and machine initials.
    • Multi-select filters for Status, Machine, Severity, Priority, Assignee, Owner, and Reporter.
    • Date range filtering for Created and Modified dates.
    • Consistency filter.
  • URL Synchronization: All filter states are synced to the URL, making views shareable.
  • Responsive Design:
    • Dynamic column hiding on the issue list table based on available width.
    • Responsive search bar with collapsing filter badges.
  • Database Integration:
    • Efficient query building using Drizzle ORM.
    • Implemented missing 'Owner' filter logic with exists subqueries.

cleanup

  • Removed all mockup components and pages (src/components/mockups, src/app/(app)/mockup).

Verification

  • Added integration tests for all filter types, including the new Owner filter.
  • Verified unit tests for URL parsing logic.
  • E2E tests included for filter interactions.

- Multi-select filters for all issue attributes
- Free-form search across title/description/machine
- Status groups (New/In Progress/Closed) + individual statuses
- Expandable 'More Filters' section for advanced filters
- Desktop-first responsive layout
- URL-based filter state for shareability
- Cached data fetching pattern
- Complete implementation phases and testing strategy
Copilot AI review requested due to automatic review settings January 19, 2026 21:11
@vercel
Copy link

vercel bot commented Jan 19, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
pin-point Ready Ready Preview, Comment Jan 20, 2026 11:04pm

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements a comprehensive issue search and filtering system for the Issues List page, replacing temporary mockup controls with a production-ready implementation featuring URL-synchronized filters, responsive design, and multi-select filter controls.

Changes:

  • Added full search and filtering functionality with 10+ filter types (search, status, machine, severity, priority, assignee, owner, reporter, consistency, date ranges)
  • Implemented URL synchronization for all filter states to enable shareable links
  • Created responsive UI components with dynamic column hiding and collapsing filter badges
  • Built efficient database query layer using Drizzle ORM with proper WHERE conditions and pagination

Reviewed changes

Copilot reviewed 20 out of 22 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
src/lib/issues/filters.ts Core filter parsing and query building logic with URL parameter handling
src/components/issues/IssueFilters.tsx Main filter UI component with responsive search bar and multi-select dropdowns
src/components/issues/IssueList.tsx Enhanced issue table with inline editing, dynamic columns, and pagination
src/components/ui/multi-select.tsx Reusable multi-select component with grouped options and search
src/components/ui/popover.tsx Popover primitive for dropdown UI elements
src/components/ui/command.tsx Command palette component for searchable lists
src/components/ui/checkbox.tsx Checkbox component with indeterminate state support
src/components/ui/calendar.tsx Calendar component for date range selection
src/components/ui/date-range-picker.tsx Date range picker combining calendar and popover
src/app/(app)/issues/page.tsx Updated issues page with new filtering, owner filter EXISTS query, and pagination
src/test/unit/lib/issues/filters.test.ts Unit tests for filter parsing and validation logic
src/test/integration/supabase/issue-filtering.test.ts Integration tests for database filtering queries
tsconfig.json Excluded mockup directories from TypeScript compilation
eslint.config.mjs Excluded mockup directories from linting
package.json Added dependencies: cmdk, react-day-picker, @radix-ui/react-checkbox, @radix-ui/react-popover
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment on lines 178 to 185
React.useEffect(() => {
const timer = window.setTimeout(() => {
if (search !== (filters.q ?? "")) {
pushFilters({ q: search, page: 1 });
}
}, 300);
return () => window.clearTimeout(timer);
}, [search]);
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

The debounced search effect is missing a dependency on 'pushFilters'. This could lead to stale closures where the effect captures an old version of the function. Add 'pushFilters' to the dependency array, or use a ref to store the function if it's intentionally excluded.

Copilot uses AI. Check for mistakes.
// Sync search state when filters prop changes (e.g. back button)
React.useEffect(() => {
setSearch(filters.q ?? "");
}, [filters.q]);
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

The effect syncing search state is missing 'setSearch' in its dependency array. While setSearch from useState is typically stable, React's exhaustive-deps rule expects it to be included for completeness.

Suggested change
}, [filters.q]);
}, [filters.q, setSearch]);

Copilot uses AI. Check for mistakes.
Comment on lines 346 to 412
React.useEffect(() => {
if (!searchBarRef.current) return;

const calculateLayout = (): void => {
window.requestAnimationFrame(() => {
if (!searchBarRef.current) return;
const containerWidth = searchBarRef.current.offsetWidth;
if (containerWidth === 0) return;

if (badgeList.length === 0) {
setVisibleBadgeCount(0);
setBadgeAreaWidth(0);
return;
}

const leftPadding = 12; // px-3 left
const rightPadding = 12; // px-3 right
const iconWidth = 16; // search icon
const iconGap = 8; // gap after icon
const plusBadgeWidth = 36; // Reserved for "+X"
const textBuffer = 10; // Space after text before collision

const textStartPosition = leftPadding + iconWidth + iconGap;
const textEndPosition = textStartPosition + textWidth + textBuffer;
const badgeAreaRightEdge = containerWidth - rightPadding;
const maxBadgeSpace = badgeAreaRightEdge - textEndPosition;

const badgeGap = 6;
const badgeWidths = badgeList.map((b) => b.label.length * 8 + 34);

let totalNeeded = badgeWidths.reduce((a, b) => a + b + badgeGap, 0);
if (badgeWidths.length > 0) totalNeeded -= badgeGap;

if (totalNeeded <= maxBadgeSpace) {
setVisibleBadgeCount(badgeList.length);
setBadgeAreaWidth(totalNeeded);
return;
}

const spaceForVisible = Math.max(
0,
maxBadgeSpace - plusBadgeWidth - badgeGap
);
let used = 0;
let count = 0;
for (const w of badgeWidths) {
if (used + w <= spaceForVisible) {
used += w + badgeGap;
count++;
} else {
break;
}
}

setVisibleBadgeCount(count);
const visibleWidth = badgeWidths
.slice(0, count)
.reduce((a, b) => a + b + badgeGap, 0);
setBadgeAreaWidth(visibleWidth + plusBadgeWidth + badgeGap);
});
};

calculateLayout();
const ro = new ResizeObserver(calculateLayout);
ro.observe(searchBarRef.current);
return () => ro.disconnect();
}, [badgeList, textWidth]);
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

The badge collision calculation effect is missing dependencies: 'badgeList' and 'textWidth'. While badgeList is recalculated on each render, the effect should properly declare these dependencies to avoid stale closures and ensure the calculation runs when these values change.

Copilot uses AI. Check for mistakes.
const [stableIds, setStableIds] = React.useState<string[]>([]);
React.useEffect(() => {
setStableIds(issues.map((i) => i.id));
}, [sort, page, pageSize, totalCount]);
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

The stable order tracking effect is missing 'issues' in its dependency array. Currently it only updates when sort, page, pageSize, or totalCount change, but the effect body directly references 'issues'. This could lead to stale issue IDs being used when issues data changes without pagination or sort changes.

Suggested change
}, [sort, page, pageSize, totalCount]);
}, [issues, sort, page, pageSize, totalCount]);

Copilot uses AI. Check for mistakes.
conditions.push(eq(issues.reportedBy, filters.reporter));
}

// TODO: Owner filter (requires join with machines)
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

The TODO comment indicates that the Owner filter is not implemented in this function, but the PR description and integration tests suggest it should be. The owner filter is actually implemented in the main issues page using an EXISTS subquery (lines 56-70 in page.tsx), but the comment is misleading since it suggests this feature is incomplete.

Suggested change
// TODO: Owner filter (requires join with machines)
// Note: The owner filter is implemented in the main issues page query
// (page.tsx) using an EXISTS subquery that joins machines to issues.
// This helper only builds conditions on the issues table, so it does not
// apply the owner filter directly.

Copilot uses AI. Check for mistakes.
Comment on lines 152 to 155
// Correctly cast OPEN_STATUSES to IssueStatus[] to avoid readonly mismatch
conditions.push(
inArray(issues.status, [...OPEN_STATUSES] as unknown as IssueStatus[])
);
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

Unsafe type casting using 'as unknown as' to bypass TypeScript's type checking. The OPEN_STATUSES constant is readonly, but inArray expects a mutable array. Consider using Array.from() or [...OPEN_STATUSES] without the unsafe cast, or adjust the type definition of OPEN_STATUSES to be non-readonly if it's safe to do so.

Suggested change
// Correctly cast OPEN_STATUSES to IssueStatus[] to avoid readonly mismatch
conditions.push(
inArray(issues.status, [...OPEN_STATUSES] as unknown as IssueStatus[])
);
conditions.push(inArray(issues.status, [...OPEN_STATUSES]));

Copilot uses AI. Check for mistakes.
Comment on lines 161 to 169
let newSort = column;
if (sort === `${column}_desc`) {
newSort = `${column}_asc`;
} else if (sort === `${column}_asc`) {
newSort = `${column}_desc`;
} else {
// Default to desc for new column
newSort = `${column}_desc`;
}
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

The initial value of newSort is unused, since it is always overwritten.

Suggested change
let newSort = column;
if (sort === `${column}_desc`) {
newSort = `${column}_asc`;
} else if (sort === `${column}_asc`) {
newSort = `${column}_desc`;
} else {
// Default to desc for new column
newSort = `${column}_desc`;
}
const newSort =
sort === `${column}_desc`
? `${column}_asc`
: sort === `${column}_asc`
? `${column}_desc`
: `${column}_desc`;

Copilot uses AI. Check for mistakes.

calculateLayout();

const resizeObserver = new ResizeObserver(calculateLayout);
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

Superfluous argument passed to default constructor of class ResizeObserver.

Copilot uses AI. Check for mistakes.
};

calculateLayout();
const ro = new ResizeObserver(calculateLayout);
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

Superfluous argument passed to default constructor of class ResizeObserver.

Copilot uses AI. Check for mistakes.
Comment on lines +144 to +146
const observer = new ResizeObserver(() => {
window.requestAnimationFrame(calculateLayout);
});
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

Superfluous argument passed to default constructor of class ResizeObserver.

Copilot uses AI. Check for mistakes.
- Add missing dependencies to useEffect hooks in IssueFilters and IssueList

- Refine sorting logic in IssueList

- Improve type safety in filters.ts by removing unsafe cast
Splits filtering logic into shared (filters.ts) and server-only (filters-queries.ts) to avoid importing database modules in Client Components.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 18 out of 20 changed files in this pull request and generated 8 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (2)

src/lib/issues/filters.ts:207

  • The exists subquery for owner filtering embeds a reference to the db instance directly in the query building function. This creates a tight coupling and makes the function harder to test in isolation. Consider passing the db instance as a parameter or restructuring to separate query building from execution.
    src/lib/issues/filters.ts:171
  • The default status filter logic shows open statuses when no status parameter is provided, but this behavior is embedded in buildWhereConditions rather than in parseIssueFilters. This creates an implicit default that's not reflected in the parsed filters object, making the state less transparent. Consider moving this default to parseIssueFilters so that filters.status explicitly contains the default open statuses when no status param exists.

Comment on lines +183 to +190
React.useEffect(() => {
const timer = window.setTimeout(() => {
if (search !== (filters.q ?? "")) {
pushFilters({ q: search, page: 1 });
}
}, 300);
return () => window.clearTimeout(timer);
}, [search, filters.q, pushFilters]);
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

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

The pushFilters function is included in the dependency array, but because it's not memoized with useCallback, the dependency will change on every render. This is an ESLint exhaustive-deps violation waiting to happen. Either remove it from the dependencies (if using useCallback), or add an ESLint disable comment with justification.

Copilot uses AI. Check for mistakes.
Comment on lines +112 to +116
if (!isNaN(d.getTime())) filters.updatedFrom = d;
}

const updatedTo = params.get("updated_to");
if (updatedTo) {
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

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

Missing error handling for the date parsing. If new Date(createdFrom) receives an invalid date string, it will create an Invalid Date object that passes the isNaN() check on line 115 but could cause issues downstream. Consider adding more robust date validation or using a date parsing library like date-fns/parse with a specific format.

Copilot uses AI. Check for mistakes.
className={cn("overflow-hidden p-0", className)}
showCloseButton={showCloseButton}
>
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

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

There's a typo in the className: **:data-[slot=command-input-wrapper]:h-12 contains **: which is not valid Tailwind CSS syntax. This should likely be [&_[data-slot=command-input-wrapper]]:h-12 to match the pattern of the other selectors in this className string.

Copilot uses AI. Check for mistakes.
Comment on lines 141 to 180
const pushFilters = (newFilters: Partial<FilterState>): void => {
const params = new URLSearchParams();
const merged = { ...filters, ...newFilters };

if (merged.q) params.set("q", merged.q);
if (merged.status && merged.status.length > 0)
params.set("status", merged.status.join(","));
if (merged.machine && merged.machine.length > 0)
params.set("machine", merged.machine.join(","));
if (merged.severity && merged.severity.length > 0)
params.set("severity", merged.severity.join(","));
if (merged.priority && merged.priority.length > 0)
params.set("priority", merged.priority.join(","));
if (merged.assignee && merged.assignee.length > 0)
params.set("assignee", merged.assignee.join(","));
if (merged.owner && merged.owner.length > 0)
params.set("owner", merged.owner.join(","));
if (merged.reporter) params.set("reporter", merged.reporter);
if (merged.consistency && merged.consistency.length > 0)
params.set("consistency", merged.consistency.join(","));

if (merged.createdFrom)
params.set("created_from", merged.createdFrom.toISOString());
if (merged.createdTo)
params.set("created_to", merged.createdTo.toISOString());

if (merged.updatedFrom)
params.set("updated_from", merged.updatedFrom.toISOString());
if (merged.updatedTo)
params.set("updated_to", merged.updatedTo.toISOString());

if (merged.sort && merged.sort !== "updated_desc")
params.set("sort", merged.sort);
if (merged.page && merged.page > 1)
params.set("page", merged.page.toString());
if (merged.pageSize && merged.pageSize !== 15)
params.set("page_size", merged.pageSize.toString());

router.push(`${pathname}?${params.toString()}`);
};
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

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

The pushFilters function is defined inside the component body but is included in the dependency array of useEffect. This creates a new function reference on every render, causing the effect to run unnecessarily. The function should be wrapped in useCallback to maintain a stable reference.

Suggested change
const pushFilters = (newFilters: Partial<FilterState>): void => {
const params = new URLSearchParams();
const merged = { ...filters, ...newFilters };
if (merged.q) params.set("q", merged.q);
if (merged.status && merged.status.length > 0)
params.set("status", merged.status.join(","));
if (merged.machine && merged.machine.length > 0)
params.set("machine", merged.machine.join(","));
if (merged.severity && merged.severity.length > 0)
params.set("severity", merged.severity.join(","));
if (merged.priority && merged.priority.length > 0)
params.set("priority", merged.priority.join(","));
if (merged.assignee && merged.assignee.length > 0)
params.set("assignee", merged.assignee.join(","));
if (merged.owner && merged.owner.length > 0)
params.set("owner", merged.owner.join(","));
if (merged.reporter) params.set("reporter", merged.reporter);
if (merged.consistency && merged.consistency.length > 0)
params.set("consistency", merged.consistency.join(","));
if (merged.createdFrom)
params.set("created_from", merged.createdFrom.toISOString());
if (merged.createdTo)
params.set("created_to", merged.createdTo.toISOString());
if (merged.updatedFrom)
params.set("updated_from", merged.updatedFrom.toISOString());
if (merged.updatedTo)
params.set("updated_to", merged.updatedTo.toISOString());
if (merged.sort && merged.sort !== "updated_desc")
params.set("sort", merged.sort);
if (merged.page && merged.page > 1)
params.set("page", merged.page.toString());
if (merged.pageSize && merged.pageSize !== 15)
params.set("page_size", merged.pageSize.toString());
router.push(`${pathname}?${params.toString()}`);
};
const pushFilters = React.useCallback(
(newFilters: Partial<FilterState>): void => {
const params = new URLSearchParams();
const merged = { ...filters, ...newFilters };
if (merged.q) params.set("q", merged.q);
if (merged.status && merged.status.length > 0)
params.set("status", merged.status.join(","));
if (merged.machine && merged.machine.length > 0)
params.set("machine", merged.machine.join(","));
if (merged.severity && merged.severity.length > 0)
params.set("severity", merged.severity.join(","));
if (merged.priority && merged.priority.length > 0)
params.set("priority", merged.priority.join(","));
if (merged.assignee && merged.assignee.length > 0)
params.set("assignee", merged.assignee.join(","));
if (merged.owner && merged.owner.length > 0)
params.set("owner", merged.owner.join(","));
if (merged.reporter) params.set("reporter", merged.reporter);
if (merged.consistency && merged.consistency.length > 0)
params.set("consistency", merged.consistency.join(","));
if (merged.createdFrom)
params.set("created_from", merged.createdFrom.toISOString());
if (merged.createdTo)
params.set("created_to", merged.createdTo.toISOString());
if (merged.updatedFrom)
params.set("updated_from", merged.updatedFrom.toISOString());
if (merged.updatedTo)
params.set("updated_to", merged.updatedTo.toISOString());
if (merged.sort && merged.sort !== "updated_desc")
params.set("sort", merged.sort);
if (merged.page && merged.page > 1)
params.set("page", merged.page.toString());
if (merged.pageSize && merged.pageSize !== 15)
params.set("page_size", merged.pageSize.toString());
router.push(`${pathname}?${params.toString()}`);
},
[filters, router, pathname]
);

Copilot uses AI. Check for mistakes.
Comment on lines +49 to +52
// Note: The clear button disappears/reappears during state changes, use force click
const clearButton = page.getByRole("button", { name: "Clear" });
await expect(clearButton).toBeVisible();
await clearButton.click({ force: true });
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

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

Using force: true in E2E tests is an anti-pattern that bypasses Playwright's actionability checks. This can lead to flaky tests that pass even when the element isn't actually interactable by users. Remove the force option and instead wait for the element to be stable and visible before clicking.

Suggested change
// Note: The clear button disappears/reappears during state changes, use force click
const clearButton = page.getByRole("button", { name: "Clear" });
await expect(clearButton).toBeVisible();
await clearButton.click({ force: true });
// Note: The clear button disappears/reappears during state changes; wait for it to be stable before clicking
const clearButton = page.getByRole("button", { name: "Clear" });
await clearButton.waitFor({ state: "visible" });
await expect(clearButton).toBeEnabled();
await clearButton.click();

Copilot uses AI. Check for mistakes.
Comment on lines +55 to +123
test.skip("should inline-edit issues (Flaky Env)", async ({ page }) => {
const title1 = "Ball stuck in Thing's box";
await page.goto("/issues");

// 4. Test Inline Editing & Stable Sorting
// Isolate TAF-01
await page.getByPlaceholder("Search issues...").fill("TAF-01"); // Search by ID
await page.keyboard.press("Enter");

// Change Priority from... whatever it is to something else.
// TAF-01 doesn't have explicit priority seeded, defaults to Low usually?
// Actually schema defaults to 'low'.
// Let's assume it has some priority. Use the button in the priority column.

// Find the priority cell. The 4th column is priority.
// Simpler: find the badge inside the row.
const row = page.getByRole("row", { name: title1 });

// We don't know the exact current priority, so lets just pick the priority dropdown trigger
// It will have a chevron-down or similar, but accessible roles are tricky.
// The cell itself is a button.
// Let's rely on test-ids if we added them?
// We didn't add test-ids to the cells in this change, relying on role="row" and position might be safer.
// Or we can query by the priority text. Default is likely 'Low' or null.

// Let's inspect the seed: it doesn't specify priority, so default 'low'.
// Wait, let's just create a clearer test case by interacting with the cell that has "Low" text.
// If it's not Low, we fail, which is fine as it documents assumption.
const priorityTrigger = row
.getByRole("button")
.filter({ hasText: /Low|Medium|High/ })
.first();
await expect(priorityTrigger).toBeVisible();

// Click and change
await priorityTrigger.click();
await page.getByRole("menuitem", { name: "High" }).click();

// Toast check - wait for it to ensure server processed it
// await expect(page.getByText("Issue updated")).toBeVisible({ timeout: 10000 });

// Verify change persisted UI (Optimistic)
await expect(
row.getByRole("button").filter({ hasText: "High" })
).toBeVisible();

// Verify persistence after reload
await page.reload();
await expect(
page
.getByRole("row", { name: title1 })
.getByRole("button")
.filter({ hasText: "High" })
).toBeVisible();

// 5. Test Assignee Inline Edit
// TAF-01 doesn't have assignee in seed (reportedBy is member, but assignedTo is null)
const assigneeCell = row
.getByRole("button")
.filter({ hasText: /Unassigned/i });
await assigneeCell.click();
await page.getByRole("menuitem", { name: TEST_USERS.admin.name }).click();

// Verify Update
await expect(page.getByText("Issue assigned")).toBeVisible();
await expect(
row.getByRole("button").filter({ hasText: TEST_USERS.admin.name })
).toBeVisible();
});
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

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

The test is marked as skipped with reason "(Flaky Env)", but skipped tests should not be committed to the codebase. Either fix the test to be reliable or remove it entirely. If environmental issues are the cause, those should be addressed separately.

Copilot uses AI. Check for mistakes.
Comment on lines +199 to +214
| Parameter | Type | Example |
| ------------- | --------------- | ------------------------------- |
| `q` | string | `q=flipper` |
| `status` | comma-separated | `status=new,confirmed` |
| `machine` | comma-separated | `machine=TZ,MM,AFM` |
| `severity` | comma-separated | `severity=major,unplayable` |
| `priority` | comma-separated | `priority=high,medium` |
| `assignee` | comma-separated | `assignee=uuid1,uuid2` |
| `owner` | comma-separated | `owner=uuid1` |
| `reporter` | string | `reporter=uuid` or `anonymous` |
| `consistency` | comma-separated | `consistency=frequent,constant` |
| `date_from` | ISO date | `date_from=2026-01-01` |
| `date_to` | ISO date | `date_to=2026-12-31` |
| `sort` | string | `sort=updated_desc` |
| `page` | number | `page=2` |
| `page_size` | number | `page_size=25` |
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

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

The date parameter names don't match the documented API. Line 210 references date_from and date_to in the documentation, but the actual code uses created_from, created_to, updated_from, and updated_to. This inconsistency between documentation and implementation will cause confusion.

Suggested change
| Parameter | Type | Example |
| ------------- | --------------- | ------------------------------- |
| `q` | string | `q=flipper` |
| `status` | comma-separated | `status=new,confirmed` |
| `machine` | comma-separated | `machine=TZ,MM,AFM` |
| `severity` | comma-separated | `severity=major,unplayable` |
| `priority` | comma-separated | `priority=high,medium` |
| `assignee` | comma-separated | `assignee=uuid1,uuid2` |
| `owner` | comma-separated | `owner=uuid1` |
| `reporter` | string | `reporter=uuid` or `anonymous` |
| `consistency` | comma-separated | `consistency=frequent,constant` |
| `date_from` | ISO date | `date_from=2026-01-01` |
| `date_to` | ISO date | `date_to=2026-12-31` |
| `sort` | string | `sort=updated_desc` |
| `page` | number | `page=2` |
| `page_size` | number | `page_size=25` |
| Parameter | Type | Example |
| --------------- | --------------- | ------------------------------- |
| `q` | string | `q=flipper` |
| `status` | comma-separated | `status=new,confirmed` |
| `machine` | comma-separated | `machine=TZ,MM,AFM` |
| `severity` | comma-separated | `severity=major,unplayable` |
| `priority` | comma-separated | `priority=high,medium` |
| `assignee` | comma-separated | `assignee=uuid1,uuid2` |
| `owner` | comma-separated | `owner=uuid1` |
| `reporter` | string | `reporter=uuid` or `anonymous` |
| `consistency` | comma-separated | `consistency=frequent,constant` |
| `created_from` | ISO date | `created_from=2026-01-01` |
| `created_to` | ISO date | `created_to=2026-12-31` |
| `updated_from` | ISO date | `updated_from=2026-01-01` |
| `updated_to` | ISO date | `updated_to=2026-12-31` |
| `sort` | string | `sort=updated_desc` |
| `page` | number | `page=2` |
| `page_size` | number | `page_size=25` |

Copilot uses AI. Check for mistakes.
};

calculateLayout();
const ro = new ResizeObserver(calculateLayout);
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

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

Superfluous argument passed to default constructor of class ResizeObserver.

Copilot uses AI. Check for mistakes.
- Implemented case-insensitive search using ilike
- Added Unassigned option to Assignee dropdown
- Added Watching filter checkbox (6th advanced filter)
- Redesigned advanced filters with 3-column grid for better spacing
- Made default status filter (New + In Progress) explicitly visible in UI
- Updated date range labels to 'Created' and 'Modified'
- Memoized pushFilters with useCallback to prevent unnecessary re-renders
- Fixed command.tsx className typo (**:data- syntax)
- Removed duplicate imports and unused code
Search now includes:
- Issue description
- Reporter name and email (both user profiles and invited users)
- Assignee name and email
- Comment content

This provides comprehensive search across all issue-related text. Performance can be monitored and optimized if needed.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 18 out of 20 changed files in this pull request and generated 9 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment on lines +35 to +44
if (filters.q) {
const search = `%${filters.q}%`;
const searchConditions = [
// Issue fields
ilike(issues.title, search),
ilike(issues.description, search),
ilike(issues.machineInitials, search),
ilike(issues.reporterName, search),
ilike(issues.reporterEmail, search),
];
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

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

The search query uses ILIKE with user-provided input wrapped in wildcards, which could be vulnerable to SQL injection through special LIKE metacharacters (%, ). While Drizzle provides parameter binding protection, user input like "test%" or "test" will be interpreted as LIKE patterns rather than literal strings. Consider escaping these special characters before wrapping in wildcards.

Copilot uses AI. Check for mistakes.
Comment on lines +214 to +217
if (filters.createdTo) {
const endOfDay = new Date(filters.createdTo);
endOfDay.setUTCHours(23, 59, 59, 999);
conditions.push(lte(issues.createdAt, endOfDay));
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

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

The date range filter modifies the filter object in-place by creating a new Date and mutating its time components. This could lead to unexpected behavior if the same filters object is reused. Consider creating a new Date object explicitly instead of mutating the input: const endOfDay = new Date(filters.createdTo.getTime()); endOfDay.setUTCHours(23, 59, 59, 999);

Copilot uses AI. Check for mistakes.
Comment on lines +224 to +227
if (filters.updatedTo) {
const endOfDay = new Date(filters.updatedTo);
endOfDay.setUTCHours(23, 59, 59, 999);
conditions.push(lte(issues.updatedAt, endOfDay));
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

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

The same date mutation issue exists for updatedTo filtering. See comment on createdTo filter above.

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,825 @@
"use client";
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

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

This component violates the Server-First principle (#1000002). The entire IssueList component with 825 lines is marked as "use client" for functionality that could be split into smaller client islands. Most of the rendering logic (table structure, data display) could remain server-side, with only interactive elements like dropdown menus and pagination buttons as client components. This increases bundle size and prevents server-side rendering optimizations.

Copilot generated this review using guidance from repository custom instructions.
Comment on lines +467 to +475
<input
ref={inputRef}
placeholder="Search issues..."
data-testid="issue-search"
className="flex-1 bg-transparent border-0 text-sm focus:outline-none placeholder:text-muted-foreground relative z-10"
style={{ paddingRight: `${badgeAreaWidth}px` }}
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

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

Missing input validation on user-provided search text. The search input should sanitize or limit special characters, maximum length, and potentially filter malicious patterns before being used in database queries.

Copilot uses AI. Check for mistakes.
Comment on lines 55 to 124
// Search in reporter's user profile name/email
searchConditions.push(
exists(
db
.select()
.from(userProfiles)
.where(
and(
eq(userProfiles.id, issues.reportedBy),
or(
ilike(userProfiles.name, search),
ilike(userProfiles.email, search)
)
)
)
)
);

// Search in invited reporter's name/email
searchConditions.push(
exists(
db
.select()
.from(invitedUsers)
.where(
and(
eq(invitedUsers.id, issues.invitedReportedBy),
or(
ilike(invitedUsers.name, search),
ilike(invitedUsers.email, search)
)
)
)
)
);

// Search in assignee's user profile name/email
searchConditions.push(
exists(
db
.select()
.from(userProfiles)
.where(
and(
eq(userProfiles.id, issues.assignedTo),
or(
ilike(userProfiles.name, search),
ilike(userProfiles.email, search)
)
)
)
)
);

// Search in issue comments
searchConditions.push(
exists(
db
.select()
.from(issueComments)
.where(
and(
eq(issueComments.issueId, issues.id),
ilike(issueComments.content, search)
)
)
)
);

conditions.push(or(...searchConditions)!);
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

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

This N+1 query pattern exists because for every search match, multiple EXISTS subqueries are executed separately for userProfiles, invitedUsers, issueComments. This could cause severe performance degradation with large datasets. Consider using LEFT JOINs instead of EXISTS subqueries, or combining all search conditions into a single more efficient query structure.

Copilot uses AI. Check for mistakes.
Comment on lines 173 to 222
{(() => {
const totalPages = Math.ceil(totalCount / filters.pageSize!);
const currentPage = filters.page!;
const pages: (number | string)[] = [];

if (totalPages <= 7) {
for (let i = 1; i <= totalPages; i++) pages.push(i);
} else {
pages.push(1);
if (currentPage > 3) pages.push("...");

const start = Math.max(2, currentPage - 1);
const end = Math.min(totalPages - 1, currentPage + 1);

for (let i = start; i <= end; i++) {
if (!pages.includes(i)) pages.push(i);
}

if (currentPage < totalPages - 2) pages.push("...");
if (!pages.includes(totalPages)) pages.push(totalPages);
}

return pages.map((p, i) => {
if (p === "...") {
return (
<span
key={`dots-${i}`}
className="w-9 h-9 flex items-center justify-center text-muted-foreground"
>
...
</span>
);
}
const pageNum = p as number;
return (
<Link
key={pageNum}
href={{ query: { ...rawParams, page: pageNum } }}
className={cn(
"inline-flex items-center justify-center rounded-md text-sm font-medium h-9 w-9",
filters.page === pageNum
? "bg-primary text-primary-foreground shadow"
: "hover:bg-accent hover:text-accent-foreground border border-transparent"
)}
>
{pageNum}
</Link>
);
});
})()}
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

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

The pagination implementation in the issues page has inline complex logic for calculating page ranges that should be extracted into a reusable utility function. This violates maintainability principles by having business logic mixed with presentation code and makes the component harder to test.

Copilot uses AI. Check for mistakes.
Comment on lines +144 to +151
const observer = new ResizeObserver(() => {
window.requestAnimationFrame(calculateLayout);
});

calculateLayout();
observer.observe(containerRef.current);

return () => observer.disconnect();
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

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

Superfluous argument passed to default constructor of class ResizeObserver.

Suggested change
const observer = new ResizeObserver(() => {
window.requestAnimationFrame(calculateLayout);
});
calculateLayout();
observer.observe(containerRef.current);
return () => observer.disconnect();
const handleResize = (): void => {
window.requestAnimationFrame(calculateLayout);
};
calculateLayout();
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};

Copilot uses AI. Check for mistakes.
};

calculateLayout();
const ro = new ResizeObserver(calculateLayout);
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

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

Superfluous argument passed to default constructor of class ResizeObserver.

Copilot uses AI. Check for mistakes.
- Updated issue-list.spec.ts to expect default status badges (New, Confirmed) to be visible on landing
- Fixed syntax error in smoke tests
- Verified all 48 active smoke tests pass
@timothyfroehlich
Copy link
Owner Author

Code review

Found 4 issues:

  1. Non-null assertion operators violate ts-strictest (AGENTS.md says "Type Safety: No any, no !, no unsafe as. This project uses ts-strictest.")

conditions.push(or(...searchConditions)!);
}

inArray(issues.assignedTo, actualAssignees)
)!
);
} else if (hasUnassigned) {

const statusConfig = sc[issue.status]!;
const severityConfig = sv[issue.severity]!;
const priorityConfig = pr[issue.priority]!;

  1. Unsafe type assertions violate ts-strictest (AGENTS.md says "Type Safety: No any, no !, no unsafe as. This project uses ts-strictest.")

const issuesList = issuesListRaw as IssueListItem[];

{stableIssues.map((issue) => {
const sc = STATUS_CONFIG as Record<
string,
{ label: string; icon: LucideIcon; iconColor: string }
>;
const sv = SEVERITY_CONFIG as Record<
string,
{ label: string; icon: LucideIcon; iconColor: string }
>;
const pr = PRIORITY_CONFIG as Record<
string,
{ label: string; icon: LucideIcon; iconColor: string }
>;

onChange={(val) =>
pushFilters({ status: val as IssueStatus[], page: 1 })
}

  1. Branch missing performance-critical database indexes from main (indexes on severity, priority, machine name, and composite comment index were added in PRs ⚡ Bolt: Add database indexes for issue severity and priority #794 and ⚡ Bolt: Add composite index for comments and name index for machines #779 after branch diverged)

assignedToIdx: index("idx_issues_assigned_to").on(t.assignedTo),
reportedByIdx: index("idx_issues_reported_by").on(t.reportedBy),
statusIdx: index("idx_issues_status").on(t.status),
createdAtIdx: index("idx_issues_created_at").on(t.createdAt),
invitedReportedByIdx: index("idx_issues_invited_reported_by").on(
t.invitedReportedBy
),
})

  1. Migration workflow violation - migration files deleted instead of following proper workflow (AGENTS.md says "Migrations ONLY: Never use drizzle-kit push. Use db:generate + db:migrate.")

https://github.com/timothyfroehlich/PinPoint/blob/50a93c6b583f30031d1c16b1c87cb9bff6f6a0af/drizzle/meta/_journal.json#L1-L30

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 23 out of 25 changed files in this pull request and generated 9 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment on lines +78 to +85
const assignee = params.get("assignee")?.split(",");
if (assignee) filters.assignee = assignee;

const owner = params.get("owner")?.split(",");
if (owner) filters.owner = owner;

const reporter = params.get("reporter")?.split(",");
if (reporter) filters.reporter = reporter;
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

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

The filter parsing for assignee, owner, and reporter accepts any UUID without validation. If an invalid UUID is passed, it will be sent to the database query which could cause errors. Consider validating that the values are valid UUIDs or adding the special "UNASSIGNED" keyword check for owner and reporter filters as well.

Copilot uses AI. Check for mistakes.
Comment on lines +28 to +30
* Builds an array of Drizzle SQL conditions from filters
* This should ONLY be called on the server.
*/
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

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

The buildWhereConditions function defaults to OPEN_STATUSES when no status filter is provided (lines 169-173). However, this behavior should be explicitly documented in the function's JSDoc comment, as it's a significant default that affects query results.

Suggested change
* Builds an array of Drizzle SQL conditions from filters
* This should ONLY be called on the server.
*/
* Builds an array of Drizzle SQL conditions from filters.
* If no status filter is provided, the resulting conditions will restrict results to OPEN_STATUSES.
* This should ONLY be called on the server.

Copilot uses AI. Check for mistakes.
const containerRef = React.useRef<HTMLDivElement>(null);

// For managing multiple concurrent updates if needed, though we'll likely do one at a time per row/cell
const [_isPending, startTransition] = React.useTransition();
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

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

The IssueList component uses React.useTransition but doesn't utilize the isPending state from the transition (line 82 shows _isPending). This pattern suggests the transition hook might not be necessary, or the pending state should be used to show global loading indicators. Consider removing the transition if not needed or using the pending state appropriately.

Suggested change
const [_isPending, startTransition] = React.useTransition();
const [, startTransition] = React.useTransition();

Copilot uses AI. Check for mistakes.
Comment on lines +419 to +420
const badgeWidths = badgeList.map((b) => b.label.length * 8 + 34);

Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

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

The badge width calculation uses a magic number formula b.label.length * 8 + 34 to estimate badge widths (line 419). This is fragile and will break if font sizes, padding, or icon sizes change. Consider measuring actual rendered badge widths using refs and getBoundingClientRect, or document why this estimation is acceptable.

Suggested change
const badgeWidths = badgeList.map((b) => b.label.length * 8 + 34);
// Measure badge text widths using the current font to avoid magic-number estimates.
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
let badgeWidths: number[] = [];
if (context && searchBarRef.current) {
const computedStyle = window.getComputedStyle(searchBarRef.current);
// Use the same font as the search bar text; badges inherit typography from the design system.
context.font = `${computedStyle.fontWeight} ${computedStyle.fontSize} ${computedStyle.fontFamily}`;
// Approximate horizontal padding + border inside each badge.
const badgeHorizontalPadding = 24; // px, accounts for left/right padding and internal spacing
badgeWidths = badgeList.map((b) => {
const textMetrics = context.measureText(b.label);
return textMetrics.width + badgeHorizontalPadding;
});
} else {
// Fallback: if we cannot obtain a rendering context, treat badges as zero-width
// so layout does not break, even though badges may be hidden earlier than ideal.
badgeWidths = badgeList.map(() => 0);
}

Copilot uses AI. Check for mistakes.
// Sync search state when filters prop changes (e.g. back button)
React.useEffect(() => {
setSearch(filters.q ?? "");
}, [filters.q, setSearch]);
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

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

The IssueFilters component has a circular dependency in the useEffect for syncing search state. The effect depends on setSearch which is from useState and will never change, but it's included in the dependency array. This is unnecessary and could be removed to simplify the code.

Suggested change
}, [filters.q, setSearch]);
}, [filters.q]);

Copilot uses AI. Check for mistakes.
Comment on lines +43 to 50
const urlParams = new URLSearchParams();
Object.entries(rawParams).forEach(([key, value]) => {
if (Array.isArray(value)) {
urlParams.set(key, value.join(","));
} else if (value !== undefined) {
urlParams.set(key, value);
}
});
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

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

The page.tsx file converts searchParams from Record to URLSearchParams manually (lines 43-50), but this conversion doesn't handle array values correctly. If Next.js provides an array value for a param, calling value.join(",") works, but the logic should verify this is the expected behavior for all filter params that support comma-separated values.

Copilot uses AI. Check for mistakes.
Comment on lines +28 to +29
page?: number | undefined;
pageSize?: number | undefined;
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

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

The filters.page default value is defined in parseIssueFilters as 1, but the IssueList component receives filters.page which should always be defined. However, the TypeScript interface for IssueFilters defines page as number | undefined. This means defensive code is needed when using the page value or the type should be updated to guarantee it's always defined after parsing. Consider making page non-optional in the return type or add a runtime check.

Copilot uses AI. Check for mistakes.
Comment on lines +35 to +158
if (filters.q) {
const search = `%${filters.q}%`;
const searchConditions = [
// Issue fields
ilike(issues.title, search),
ilike(issues.description, search),
ilike(issues.machineInitials, search),
ilike(issues.reporterName, search),
ilike(issues.reporterEmail, search),
];

// Check if the query matches a pattern like "AFM-101" or "AFM 101"
const issuePatternMatch = /^([a-zA-Z]{1,4})[- ](\d+)$/.exec(
filters.q.trim()
);
if (issuePatternMatch) {
const initials = issuePatternMatch[1];
const num = issuePatternMatch[2];
if (initials && num) {
const issueNum = parseInt(num, 10);
if (!isNaN(issueNum)) {
const cond = and(
ilike(issues.machineInitials, initials),
eq(issues.issueNumber, issueNum)
);
if (cond) {
searchConditions.push(cond);
}
}
}
} else {
// Fallback: check if the query is just a number
const numericMatch = /^\d+$/.exec(filters.q.trim());
if (numericMatch) {
const issueNum = parseInt(numericMatch[0], 10);
if (!isNaN(issueNum)) {
searchConditions.push(eq(issues.issueNumber, issueNum));
}
}
}

// Search in reporter's user profile name/email
searchConditions.push(
exists(
db
.select()
.from(userProfiles)
.where(
and(
eq(userProfiles.id, issues.reportedBy),
or(
ilike(userProfiles.name, search),
ilike(userProfiles.email, search)
)
)
)
)
);

// Search in invited reporter's name/email
searchConditions.push(
exists(
db
.select()
.from(invitedUsers)
.where(
and(
eq(invitedUsers.id, issues.invitedReportedBy),
or(
ilike(invitedUsers.name, search),
ilike(invitedUsers.email, search)
)
)
)
)
);

// Search in assignee's user profile name/email
searchConditions.push(
exists(
db
.select()
.from(userProfiles)
.where(
and(
eq(userProfiles.id, issues.assignedTo),
or(
ilike(userProfiles.name, search),
ilike(userProfiles.email, search)
)
)
)
)
);

// Search in machine names
searchConditions.push(
exists(
db
.select()
.from(machines)
.where(
and(
eq(machines.initials, issues.machineInitials),
ilike(machines.name, search)
)
)
)
);

// Search in issue comments
searchConditions.push(
exists(
db
.select()
.from(issueComments)
.where(
and(
eq(issueComments.issueId, issues.id),
ilike(issueComments.content, search)
)
)
)
);
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

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

The search query builds multiple exists subqueries which could result in performance issues with large datasets. Each subquery requires a separate scan. Consider if this can be optimized using JOINs or a full-text search index on the relevant columns instead of multiple EXISTS clauses with ILIKE operations.

Copilot uses AI. Check for mistakes.
Comment on lines +47 to +62
const issuePatternMatch = /^([a-zA-Z]{1,4})[- ](\d+)$/.exec(
filters.q.trim()
);
if (issuePatternMatch) {
const initials = issuePatternMatch[1];
const num = issuePatternMatch[2];
if (initials && num) {
const issueNum = parseInt(num, 10);
if (!isNaN(issueNum)) {
const cond = and(
ilike(issues.machineInitials, initials),
eq(issues.issueNumber, issueNum)
);
if (cond) {
searchConditions.push(cond);
}
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

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

The regex pattern for issue ID matching uses non-capturing groups but captures the results. The pattern [a-zA-Z]{1,4} should be [A-Z]{1,4} since machine initials are conventionally uppercase. Also, the conditional logic checking issuePatternMatch[1] and issuePatternMatch[2] after exec could fail if the regex doesn't match the expected groups.

Suggested change
const issuePatternMatch = /^([a-zA-Z]{1,4})[- ](\d+)$/.exec(
filters.q.trim()
);
if (issuePatternMatch) {
const initials = issuePatternMatch[1];
const num = issuePatternMatch[2];
if (initials && num) {
const issueNum = parseInt(num, 10);
if (!isNaN(issueNum)) {
const cond = and(
ilike(issues.machineInitials, initials),
eq(issues.issueNumber, issueNum)
);
if (cond) {
searchConditions.push(cond);
}
const issuePatternMatch = /^([A-Z]{1,4})[- ](\d+)$/.exec(
filters.q.trim()
);
if (issuePatternMatch && issuePatternMatch.length === 3) {
const [, initials, num] = issuePatternMatch;
const issueNum = Number.parseInt(num, 10);
if (!Number.isNaN(issueNum)) {
const cond = and(
ilike(issues.machineInitials, initials),
eq(issues.issueNumber, issueNum)
);
if (cond) {
searchConditions.push(cond);

Copilot uses AI. Check for mistakes.
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