Projects Page Search UX Improvements#1133
Conversation
Greptile SummaryThis PR adds client-side search, sort (by time/name), grid/list view toggle, and an archival status dropdown to the Projects page — all useful UX improvements. However, the PR replaces the existing Inertia
Confidence Score: 3/5Not safe to merge as-is — the manual CSRF token approach replaces Inertia form utilities and introduces an empty-token race condition. Two P1 findings around the same root cause (manual CSRF / raw form replacing Inertia utilities) pull the score below the P1 ceiling of 4. The UX improvements themselves are solid but the form regression needs to be addressed first. app/javascript/pages/Projects/Index.svelte — specifically the onMount CSRF fetch and the raw form in the mapping editor. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[User visits /projects] --> B{projects_data loaded?}
B -- No --> C[Skeleton loader]
B -- Yes --> D[Render filter bar]
D --> E[searchQuery input]
D --> F[IntervalSelect]
D --> G[archivalStatus Select]
D --> H[sortBy Select]
D --> I[viewMode toggle grid/list]
E & H --> J[filteredAndSortedProjects $derived.by]
J --> K[Render project cards]
G --> L[$effect: archivalStatus changed?]
L -- Yes --> M[window.location.href full page reload]
L -- No --> N[No navigation]
F --> O[router.visit partial reload]
K --> P{manage_enabled & editing?}
P -- Yes --> Q[Raw HTML form with manual csrfToken]
Q --> R[POST to update_path with _method=patch]
K --> S[Archive/Restore button]
S --> T[confirmStatusChange router.patch]
Prompt To Fix All With AIFix the following 3 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 3
app/javascript/pages/Projects/Index.svelte:163-168
**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.
### Issue 2 of 3
app/javascript/pages/Projects/Index.svelte:592-610
**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.
### Issue 3 of 3
app/javascript/pages/Projects/Index.svelte:152-154
**`$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.
Reviews (1): Last reviewed commit: "Merge remote-tracking branch 'upstream/m..." | 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!
Some of the Hackatime improvements I've wanted for a while :D