Skip to content

Use Supabase views for review queues and split reviews page into new vs. existing edits - #45

Open
tayloraj10 wants to merge 11 commits into
mainfrom
supabase-edits-views-usage
Open

tayloraj10 wants to merge 11 commits into
mainfrom
supabase-edits-views-usage

Conversation

@tayloraj10

@tayloraj10 tayloraj10 commented Aug 12, 2026 •

Copy link
Copy Markdown
Contributor

Use Supabase views for review queues and split reviews page into new vs. existing edits

Problem

The reviews and dashboard pages were pulling data by scanning the full resource_edits table directly, rather than using the purpose-built resource_edit_counts and resource_change_log views. The reviews index page also lumped new site submissions and edits to existing resources into a single table, making it harder to review either group on its own.

Solution

  • Switched the reviews and dashboard loaders to pull from the resource_edit_counts and resource_change_log views via new getEditCounts, getChangeLog, and getChangeLogForResource API methods, instead of scanning resource_edits directly.
  • Split the reviews index page into two separate tables — "New site submissions" and "Edits to existing resources" — each with its own columns, including a "Pending edits" count for resources with multiple competing edits.
  • Added a change-log audit trail to the review detail page using the new view-backed data.

Reference

Closes #43. Partially addresses #35 (splits new-resource vs. existing-edit review queues; the duplicate-detection confidence rating from #35 is not part of this PR).

Screenshots

Screenshot 2026-08-11 214826

Checklist

  • Changes have been tested locally
  • Changes have been self-reviewed

Convert the "Save changes" button on the review detail page from a
bare onClick handler to a <fetcher.Form onSubmit>, matching the
approve/reject actions that already use <Form>. The payload is a
nested object rather than flat fields, so it's still sent as JSON via
fetcher.submit, but routing it through a real form gives us proper
submit semantics (Enter-to-submit, pending state, progressive
enhancement) instead of a plain button click.

Also swap the deprecated FormEvent type for SubmitEvent, and convert
logout to a POST action (was a GET loader) triggered via a <Form> in
the sidebar, since a GET route shouldn't perform a mutation like
signing out.
- Introduced new API methods for fetching edit counts and change logs, enhancing the resource edit management capabilities.
- Updated the dashboard to utilize these new methods, allowing for a comprehensive view of pending edits and historical changes.
- Modified the reviews detail page to display the change log for individual resources, improving audit trail visibility.
- Enhanced the reviews index page to show pending edit counts for resources, aiding in resource management and review prioritization.
- Introduced separate types for edit and new row data, improving type safety and clarity.
- Updated table columns to include a submitted date for both edits and new resources, enhancing the review process.
- Refactored the ReviewQueueTable component to handle sorting and rendering more efficiently.
- Improved the overall layout by incorporating a Stack component for better spacing and organization.
- Added a submitted date column to the reviews index, improving visibility of edit timelines.
- Refactored the ReviewQueueTable component for better data handling and presentation.
- Updated type definitions for edit and new row data to enhance clarity and type safety.
Comment thread app/api/resource-edits/methods.ts Outdated
Comment thread app/api/resource-edits/methods.ts
Comment thread app/routes/authenticated/reviews/index.tsx
AnilKumar3494 and others added 6 commits September 9, 2026 13:19
The migration enforced at most one PENDING edit per resource by
overwriting the existing row on each new submission. That makes the
"Pending edits" column this PR adds permanently show 1 -- the warning
chip for competing edits can never fire.

It also destroys data: three people reporting three different things
about one resource leaves only the last, unrecoverable and unseen by
any reviewer. Returning NULL from the BEFORE INSERT trigger makes
INSERT ... RETURNING yield zero rows, which breaks any submitting
client that reads back the row it just created. And because the
trigger is SECURITY DEFINER, an anonymous submitter can overwrite
another person's pending edit, bypassing RLS.

Competing edits are a real problem, but deleting the losing
submissions hides it rather than resolving it, and the underlying
lost-update bug survives the migration anyway (an older snapshot
approved after a newer one still reverts it). Handling that belongs
in approve_edit, tracked separately.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuKg4c7DHw29iNM2BXn6B3
Top submitters bucketed every unattributed edit under "Unknown", which
then dominated the leaderboard: on current data it reads "Unknown 9"
and says nothing. Skip null submitted_by instead, so the existing empty
state renders when there is genuinely nothing to attribute.

creator is not a substitute -- it is a mirrored ResourceEntry field
carried in the proposed snapshot, so it describes who created the
resource (values are ingest sources like 'phlask_app'), not who
submitted the edit. Renamed the loader field creator -> submitter,
which is what it has actually held since this PR.

totalEdits/pendingCount now come from count queries with head: true
rather than counting fetched rows, and the loader comment no longer
claims the change log avoids scanning resource_edits -- it is a plain
view over that table, so it reads the same rows, just narrower columns.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuKg4c7DHw29iNM2BXn6B3
getChangeLogForResource relied on the view's own ORDER BY surviving an
outer filtered query, which Postgres does not guarantee -- the change
history could render in arbitrary order. Order it explicitly.

The review detail loader fetched the resource and the change log
serially, and unlike the resource fetch the change log was not guarded,
so a view error took down the whole review page. Both are supporting
context for the diff, so run them together and let each fall back to
empty.

ResourceEditQueueRow now declares mapped_resource as non-null: the view
filters on `is not null` and inner-joins resources, so the narrower type
is the accurate one. That removes the unreachable `: 0` branch behind
the competing-edits count.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuKg4c7DHw29iNM2BXn6B3
Nothing was ever setting this column: submitting clients do not send it
and the admin dashboard only reads edits, never creates them. Every row
arrives with submitted_by = null, which is why the dashboard has no
submitters to attribute and the detail page cannot say who proposed a
change.

A BEFORE INSERT trigger fills it from request.jwt.claims (email, then
sub) when it arrives blank, so attribution starts working with no
client change. A value the client sent is never overwritten, malformed
claims are caught so they cannot block a submission, and the trigger
returns NEW so INSERT ... RETURNING is unaffected.

Anonymous submissions still land with submitted_by = null -- an anon
JWT carries no identity to record -- so attributing those needs the
submitting client to send something.

Includes the matching rollback, per the supabase/rollback convention.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuKg4c7DHw29iNM2BXn6B3
Addresses review feedback on #45. getList on the resource-edits API has
had no callers since the dashboard moved to the count and queue
queries, so remove it. The review tables now key rows by the edit's own
id instead of TanStack's default array index, so row identity stays
stable when the loader revalidates with different data.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
phlask-map changed food.organization_type from an array to a single
value in January 2026, but the dashboard still typed it as an array and
rendered it in an <Autocomplete multiple>. Every food edit submitted
from the current map stores a string, so opening its review page threw
"value.map is not a function".

Organization type is now a single select, with a reader that still
accepts the old array shape. food.tags, which the map's food form sets,
is added to the type and carried through the form so saving an edit no
longer drops it. The enums now match the map and the stored data:
VESSEL replaces PITCHER (never present in the data), and HAS_FOUNTAIN
and NONOPERATIONAL are added.

The existing array rows in resources were normalized separately on
2026-10-13.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

Use supabase DB views for data pulling instead of calculating on frontend code

3 participants