Skip to content

Add drag-and-drop ordering for featured stories#1546

Open
maebeale wants to merge 1 commit into
mainfrom
maebeale/san-francisco-v4
Open

Add drag-and-drop ordering for featured stories#1546
maebeale wants to merge 1 commit into
mainfrom
maebeale/san-francisco-v4

Conversation

@maebeale
Copy link
Copy Markdown
Collaborator

@maebeale maebeale commented Jun 5, 2026

Closes #1522

What is the goal of this PR and why is this important?

  • The home page "Highlighted stories" section showed featured stories ordered by title, with no way to curate which story leads.
  • Stakeholders requested control over the display order so they can promote specific stories on the home page.

How did you approach the change?

  • Added a position column to stories, scoped via the positioning gem to the featured set (positioned on: :featured), so featured stories keep a contiguous, independently-curated order.
  • Backfilled existing rows with a contiguous per-group sequence in the migration so the gem has a valid starting state.
  • Added an admin-only Reorder featured stories page (/featured_stories) that reuses the existing sortable Stimulus controller + SortableJS drag-and-drop, mirroring the FAQ/Category pattern already in the app.
  • The reorder list intentionally shows the full featured set (published or not) so the drag index lines up exactly with the positioning scope; unpublished stories are flagged as hidden from the home page.
  • Updated Home::StoriesController to order by position instead of title, so both the authenticated (featured) and public (publicly_featured) home views respect the curated order.
  • Added an admin-only "Reorder featured" link on the stories index header.

Anything else to add?

  • Authorization reuses StoryPolicy via a new reorder? rule (super users only); non-admins are redirected to root, consistent with the app's ActionPolicy::Unauthorized handling.
  • The public (anonymous) home page draws from publicly_featured; those stories follow the same position column, so curation carries over for stories that are both featured and publicly featured.
  • Tests: model positioning specs, request specs for the reorder controller (authorization + reordering), and a home stories ordering spec — all green.

UI Testing Checklist

  • As a super user, visit Stories → "Reorder featured", drag a story to the top, and confirm the new order persists after reload.
  • Confirm the home page "Highlighted stories" section reflects the curated order.
  • Confirm non-admins cannot access /featured_stories.

Stakeholders need to curate which stories surface first in the home page
highlighted stories section. Until now featured stories were ordered by
title, leaving no way to promote a specific story.

Add a `position` column (scoped to the featured set via the positioning
gem) and an admin-only reorder page so the home page order can be
controlled directly.

Closes #1522
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 5, 2026 00:40
Comment thread app/models/story.rb
# keeps a contiguous sequence within the featured set, independent of the
# much larger pool of non-featured stories.
positioned on: :featured

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Scoped on :featured (rather than a global sequence) so the reorder list — which shows exactly the featured set — maps 1:1 to the absolute index SortableJS sends. A global position would desync once non-featured stories interleave.

authorize! Story, to: :reorder?
# Operate on the full featured set (published or not) so the drag-and-drop
# index lines up exactly with the positioning scope (positioned on: :featured).
@stories = Story.where(featured: true).order(:position).decorate
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Deliberately uses where(featured: true) (published or not), not Story.featured (published-only). The list must match the positioning scope exactly, otherwise hidden unpublished featured stories would throw off the drag index. The view flags unpublished ones.

story = Story.find(params[:id])
authorize! story, to: :reorder?
story.update!(position: params[:position])
head :no_content
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Reads params[:position] (the top-level key SortableJS PUTs as JSON). The positioning gem intercepts the assignment and re-sequences the featured group.

# positioning gem (scoped on :featured) has a valid starting state.
execute <<~SQL.squish
UPDATE stories s
JOIN (
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Backfill gives each featured group a contiguous 1..n sequence (ROW_NUMBER per featured) so the gem starts from a valid state; column is set NOT NULL only after backfill.

authorize! :home
@stories = authorized_scope(Story.published
.order(:title), with: HomePolicy)
.order(:position), with: HomePolicy)
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Order by :position instead of :title. HomePolicy still scopes to featured/publicly_featured; the curated order flows through to both the authenticated and public home views.

@maebeale maebeale marked this pull request as ready for review June 5, 2026 00:41
Copy link
Copy Markdown
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

Adds a curated (drag-and-drop) ordering mechanism for stories intended to control the display order of “Highlighted stories” on the home page by introducing a position column, a reorder UI, and updated ordering in the home stories feed.

Changes:

  • Add stories.position with backfill + ordering index, and enable per-featured positioning via the positioning gem.
  • Add an admin-only /featured_stories reorder page (Stimulus sortable + SortableJS) and an update endpoint to persist drag-and-drop reordering.
  • Update the home stories feed to order by position, and add request/model coverage for ordering + authorization.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
spec/requests/home/stories_spec.rb Verifies home stories render in curated order.
spec/requests/featured_stories_spec.rb Covers access control and update behavior for reorder endpoint.
spec/models/story_spec.rb Adds model-level specs for positioning behavior within featured/non-featured groups.
db/schema.rb Reflects new position column and new index on stories.
db/migrate/20260604143000_add_position_to_stories.rb Adds/backfills position and indexes ordering.
config/routes.rb Adds routes for featured stories reorder UI + update.
app/views/stories/index.html.erb Adds “Reorder featured” link for authorized users.
app/views/featured_stories/index.html.erb New reorder page view wiring up sortable.
app/views/featured_stories/_featured_story.html.erb New reorder list item partial (drag handle, hidden-state flag).
app/policies/story_policy.rb Adds reorder? authorization rule.
app/models/story.rb Enables positioning behavior for stories via positioned on: :featured.
app/controllers/home/stories_controller.rb Changes ordering to position for home stories feed.
app/controllers/featured_stories_controller.rb New controller for reorder page + update endpoint.

Comment on lines +9 to +14
def update
story = Story.find(params[:id])
authorize! story, to: :reorder?
story.update!(position: params[:position])
head :no_content
end
Comment on lines 6 to 9
authorize! :home
@stories = authorized_scope(Story.published
.order(:title), with: HomePolicy)
.order(:position), with: HomePolicy)
.decorate
Comment on lines +2 to +18
def up
add_column :stories, :position, :integer
add_index :stories, [ :featured, :position ]

# Backfill a contiguous 1..n sequence within each featured group so the
# positioning gem (scoped on :featured) has a valid starting state.
execute <<~SQL.squish
UPDATE stories s
JOIN (
SELECT id, ROW_NUMBER() OVER (PARTITION BY featured ORDER BY created_at, id) AS rn
FROM stories
) ranked ON ranked.id = s.id
SET s.position = ranked.rn
SQL

change_column_null :stories, :position, false
end
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.

Home page: control the order of featured stories

2 participants