Projects Page Search UX Improvements#1133
Conversation
Greptile SummaryThis PR adds several UX improvements to the Projects page: a client-side text search box, an archival-status selector (active/archived), a sort dropdown, and a grid/list view toggle. The broken-name warning is also now surfaced in list view as a compact badge.
Confidence Score: 5/5Safe to merge; all changes are client-side rendering improvements with no impact on data persistence or security boundaries. The change is entirely within a single Svelte component and touches only how project data is displayed and filtered. The derived filtering and sorting operate on already-loaded data, the archival status redirects use the existing URL-building helpers, and no new server endpoints are introduced. app/javascript/pages/Projects/Index.svelte — specifically the duplicated action-button blocks for list vs. grid view, which will need to stay in sync manually until extracted into a component. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Projects page loads] --> B{projects_data deferred?}
B -- loading --> C[Skeleton cards]
B -- ready --> D{projects_data.projects.length == 0?}
D -- yes --> E[Server empty state message]
D -- no --> F[filteredAndSortedProjects derived]
F --> G{User inputs}
G -- searchQuery change --> F
G -- sortBy change --> F
G -- archivalStatus change --> H[changeArchivedStatus]
H --> I[window.location.href = new URL]
I --> A
F --> J{filteredAndSortedProjects.length == 0?}
J -- yes --> K[No search results message]
J -- no --> L{viewMode}
L -- grid --> M[Auto-fill grid of cards]
L -- list --> N[Compact list rows]
Prompt To Fix All With AIFix the following 2 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 2
app/javascript/pages/Projects/Index.svelte:603-650
**Action buttons duplicated across list and grid modes**
The complete set of project action buttons — homepage link, GitHub link, edit/link-repo button, and archive/restore button — is rendered twice: once in the list-mode header block (here) and again in the grid-mode action bar (~lines 683–735). That's ~60 lines of near-identical markup that diverges over time. The repository guidelines suggest extracting duplicated markup into a shared component; a small `ProjectActions.svelte` accepting `project`, `show_archived`, and `viewMode` as props would serve both modes without the duplication.
### Issue 2 of 2
app/javascript/pages/Projects/Index.svelte:564-570
The `min-h-36` class (144 px) is applied unconditionally in both grid and list modes. In list mode this makes every row at least 144 px tall, producing a sparse list that looks more like a short grid than a compact view. List rows need far less vertical space — removing the minimum height in list mode lets the content dictate the row height naturally.
```suggestion
<article
class="group relative flex w-full {viewMode === 'list'
? 'flex-row items-start sm:items-center sm:justify-between'
: 'min-h-36'} overflow-hidden rounded-2xl {projectHref
? 'cursor-pointer'
: ''}"
>
```
Reviews (3): Last reviewed commit: "chore: format" | Re-trigger Greptile |
| onMount(() => { | ||
| csrfToken = | ||
| document | ||
| .querySelector("meta[name='csrf-token']") | ||
| ?.getAttribute("content") || ""; | ||
| }); |
There was a problem hiding this comment.
Manual CSRF token replaces Inertia form utilities
This PR removes the Form import from @inertiajs/svelte and instead manually reads the CSRF token from the DOM via onMount, then injects it as a hidden authenticity_token input in a raw HTML <form>. This bypasses Inertia's built-in CSRF protection, validation state, and error handling. The preferred pattern is to use router.patch (already used in confirmStatusChange) or Inertia's useForm, both of which handle CSRF automatically.
Rule Used: What: Always use Inertia's <Form> or useForm u... (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: app/javascript/pages/Projects/Index.svelte
Line: 163-168
Comment:
**Manual CSRF token replaces Inertia form utilities**
This PR removes the `Form` import from `@inertiajs/svelte` and instead manually reads the CSRF token from the DOM via `onMount`, then injects it as a hidden `authenticity_token` input in a raw HTML `<form>`. This bypasses Inertia's built-in CSRF protection, validation state, and error handling. The preferred pattern is to use `router.patch` (already used in `confirmStatusChange`) or Inertia's `useForm`, both of which handle CSRF automatically.
**Rule Used:** What: Always use Inertia's `<Form>` or `useForm` u... ([source](https://app.greptile.com/review/custom-context?memory=03426195-806a-4980-ad3d-f1c3db3c22fb))
How can I resolve this? If you propose a fix, please make it concise.| <form method="post" action={project.update_path} class="space-y-3"> | ||
| <input type="hidden" name="_method" value="patch" /> | ||
| <input type="hidden" name="authenticity_token" value={csrfToken} /> | ||
|
|
||
| <input | ||
| type="url" | ||
| name="project_repo_mapping[repo_url]" | ||
| bind:value={repoUrlDraft} | ||
| placeholder="https://github.com/owner/repo" | ||
| class="w-full rounded-lg border border-surface-200 bg-darker px-3 py-2 text-sm text-surface-content focus:border-primary focus:outline-none" | ||
| /> | ||
|
|
||
| <div class="flex gap-2"> | ||
| <Button type="submit" variant="primary" size="sm" class="flex-1">Save</Button> | ||
| <Button type="button" variant="dark" size="sm" class="flex-1" onclick={closeMappingEditor}> | ||
| Cancel | ||
| </Button> | ||
| </div> | ||
| </form> |
There was a problem hiding this comment.
Raw HTML form with manual CSRF injection
The mapping editor form uses a raw <form method="post"> with a hidden authenticity_token input populated from the manually fetched csrfToken. Because csrfToken is initialised to "" and only populated in onMount, there's a window where a rapid form submission could send an empty token. Using router.patch or Inertia's useForm — consistent with confirmStatusChange — would eliminate this race and remove the manual CSRF plumbing entirely.
Rule Used: What: Always use Inertia's <Form> or useForm u... (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: app/javascript/pages/Projects/Index.svelte
Line: 592-610
Comment:
**Raw HTML form with manual CSRF injection**
The mapping editor form uses a raw `<form method="post">` with a hidden `authenticity_token` input populated from the manually fetched `csrfToken`. Because `csrfToken` is initialised to `""` and only populated in `onMount`, there's a window where a rapid form submission could send an empty token. Using `router.patch` or Inertia's `useForm` — consistent with `confirmStatusChange` — would eliminate this race and remove the manual CSRF plumbing entirely.
**Rule Used:** What: Always use Inertia's `<Form>` or `useForm` u... ([source](https://app.greptile.com/review/custom-context?memory=03426195-806a-4980-ad3d-f1c3db3c22fb))
How can I resolve this? If you propose a fix, please make it concise.| $effect(() => { | ||
| archivalStatus = show_archived ? "archived" : "active"; | ||
| }); |
There was a problem hiding this comment.
$effect used to derive state from a prop
This $effect sets archivalStatus from show_archived, which is purely derived state and doesn't need an effect. Since changeArchivedStatus uses window.location.href (a full page reload), show_archived will always be fresh on mount. archivalStatus can simply be initialized as $state(show_archived ? "archived" : "active"), removing one effect and reducing unnecessary re-runs.
Rule Used: What: Don't use effects to derive state that can b... (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: app/javascript/pages/Projects/Index.svelte
Line: 152-154
Comment:
**`$effect` used to derive state from a prop**
This `$effect` sets `archivalStatus` from `show_archived`, which is purely derived state and doesn't need an effect. Since `changeArchivedStatus` uses `window.location.href` (a full page reload), `show_archived` will always be fresh on mount. `archivalStatus` can simply be initialized as `$state(show_archived ? "archived" : "active")`, removing one effect and reducing unnecessary re-runs.
**Rule Used:** What: Don't use effects to derive state that can b... ([source](https://app.greptile.com/review/custom-context?memory=8569b05b-35ae-4e04-9b00-50fe1f992c68))
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Co-authored-by: Copilot <copilot@github.com>
|
@greptileai review |
Some of the Hackatime improvements I've wanted for a while :D