-
Notifications
You must be signed in to change notification settings - Fork 24
Surface pending review queues on admin home #1548
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| # Summarizes the admin review queues for the dashboard: user-submitted ideas | ||
| # that have not yet been promoted into their published counterparts. Each queue | ||
| # exposes its label, pending count, and originating model so the view can link | ||
| # to the relevant index. | ||
| class PendingReviewSummary | ||
| Queue = Data.define(:label, :count, :model) | ||
|
|
||
| QUEUE_DEFINITIONS = [ | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adding a new admin review queue is intentionally a one-line change here — append |
||
| { model: StoryIdea, label: "Story ideas" }, | ||
| { model: WorkshopVariationIdea, label: "Workshop variation ideas" }, | ||
| { model: WorkshopIdea, label: "Workshop ideas" } | ||
| ].freeze | ||
|
|
||
| def queues | ||
| @queues ||= QUEUE_DEFINITIONS.map do |definition| | ||
| Queue.new( | ||
| label: definition[:label], | ||
| count: definition[:model].pending_review.count, | ||
| model: definition[:model] | ||
| ) | ||
| end | ||
| end | ||
|
|
||
| def pending_queues | ||
| queues.select { |queue| queue.count.positive? } | ||
| end | ||
|
|
||
| def total_count | ||
| queues.sum(&:count) | ||
| end | ||
|
|
||
| def any? | ||
| total_count.positive? | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| <% if review_summary.any? %> | ||
| <div class="bg-yellow-50 border border-yellow-200 text-yellow-800 rounded-xl shadow p-6 mb-6" | ||
| role="status"> | ||
| <div class="flex items-start gap-4"> | ||
| <div class="text-3xl flex-shrink-0" aria-hidden="true">🔔</div> | ||
| <div class="flex-1"> | ||
| <h2 class="text-lg font-semibold text-yellow-900"> | ||
| <%= pluralize(review_summary.total_count, "item") %> waiting for review | ||
| </h2> | ||
| <ul class="mt-3 flex flex-col gap-2"> | ||
| <% review_summary.pending_queues.each do |queue| %> | ||
| <li> | ||
| <%= link_to polymorphic_path(queue.model), | ||
| class: "inline-flex items-center gap-2 font-medium text-yellow-900 hover:underline" do %> | ||
| <span class="inline-flex items-center justify-center min-w-6 h-6 px-2 rounded-full bg-yellow-200 text-yellow-900 text-sm font-bold"> | ||
|
|
||
| <%= queue.count %> | ||
| </span> | ||
| <%= queue.label %> | ||
| <% end %> | ||
| </li> | ||
| <% end %> | ||
| </ul> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| <% end %> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,8 @@ | |
| <h1 class="text-2xl font-semibold text-gray-900">Admin home</h1> | ||
| </div> | ||
|
|
||
| <%= render "review_notifications", review_summary: @review_summary %> | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Banner renders above the existing cards so the backlog is the first thing an admin sees on login. The partial no-ops when nothing is pending, so the dashboard is unchanged when caught up. |
||
|
|
||
| <div class="bg-white border border-gray-200 rounded-xl shadow-sm p-6"> | ||
| <div class="flex flex-col gap-6 md:flex-row"> | ||
| <!-- Column 1 --> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| require "rails_helper" | ||
|
|
||
| RSpec.describe PendingReviewSummary do | ||
| describe "#queues" do | ||
| it "counts the unpromoted ideas in each review queue" do | ||
| create(:story_idea) | ||
| create(:story_idea, :with_story) | ||
| create(:workshop_variation_idea) | ||
| create(:workshop_idea) | ||
|
|
||
| summary = described_class.new | ||
| counts = summary.queues.to_h { |queue| [ queue.model, queue.count ] } | ||
|
|
||
| expect(counts).to eq( | ||
| StoryIdea => 1, | ||
| WorkshopVariationIdea => 1, | ||
| WorkshopIdea => 1 | ||
| ) | ||
| end | ||
| end | ||
|
|
||
| describe "#pending_queues" do | ||
| it "only returns queues with at least one pending item" do | ||
| create(:story_idea) | ||
|
|
||
| summary = described_class.new | ||
|
|
||
| expect(summary.pending_queues.map(&:model)).to eq([ StoryIdea ]) | ||
| end | ||
| end | ||
|
|
||
| describe "#total_count and #any?" do | ||
| it "sums the pending items across all queues" do | ||
| create(:story_idea) | ||
| create(:workshop_idea) | ||
|
|
||
| summary = described_class.new | ||
|
|
||
| expect(summary.total_count).to eq(2) | ||
| expect(summary.any?).to be(true) | ||
| end | ||
|
|
||
| it "reports nothing pending when every idea is promoted" do | ||
| create(:story_idea, :with_story) | ||
|
|
||
| summary = described_class.new | ||
|
|
||
| expect(summary.total_count).to eq(0) | ||
| expect(summary.any?).to be(false) | ||
| end | ||
| end | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Pending review" is derived from the existing promotion relationship rather than a new status column: an idea is pending until it has been promoted into a Story (the same signal the index's "Promoted to Story" column already uses). Keeps this consistent with how the rest of the app reasons about idea state and avoids a migration.