Skip to content

Projects Page Search UX Improvements#1133

Draft
matmanna wants to merge 2 commits into
hackclub:mainfrom
matmanna:projects-page
Draft

Projects Page Search UX Improvements#1133
matmanna wants to merge 2 commits into
hackclub:mainfrom
matmanna:projects-page

Conversation

@matmanna
Copy link
Copy Markdown
Member

@matmanna matmanna commented Apr 5, 2026

Some of the Hackatime improvements I've wanted for a while :D

image image image

@matmanna matmanna marked this pull request as ready for review May 2, 2026 20:28
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 2, 2026

Greptile Summary

This 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 Form with a raw HTML <form> that manually fetches and injects the CSRF token via onMount, which violates the project's Inertia form utility convention and introduces an empty-token race condition on fast submissions.

  • P1: onMount + hidden authenticity_token input replaces Inertia form handling — use router.patch or useForm instead (consistent with confirmStatusChange elsewhere in this file).
  • P1: The raw form's csrfToken is \"\" until onMount fires, so a submit before mount sends an empty token.
  • P2: First $effect (line 152) derives archivalStatus from the show_archived prop — this can be a direct $state initialiser since navigation causes a full page reload.

Confidence Score: 3/5

Not 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

Filename Overview
app/javascript/pages/Projects/Index.svelte Adds client-side search, sort, grid/list toggle, and archival status filter to the Projects page; introduces manual CSRF token via onMount and raw HTML form (replacing the previous Inertia Form), and uses $effect for state that can be derived directly.

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]
Loading
Prompt To Fix All With AI
Fix 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

Comment on lines +163 to +168
onMount(() => {
csrfToken =
document
.querySelector("meta[name='csrf-token']")
?.getAttribute("content") || "";
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 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.

Comment on lines +592 to +610
<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>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 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.

Comment on lines +152 to +154
$effect(() => {
archivalStatus = show_archived ? "archived" : "active";
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 $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!

@matmanna matmanna marked this pull request as draft May 8, 2026 11:51
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.

1 participant