Make article routing legible on the Articles page #113

Closed
opened 2026-08-01 08:47:04 +02:00 by myrmidex · 0 comments
Owner

Summary

Every row on the Articles page is a RouteArticle — a feed × channel pairing. Approving one publishes to that row's channel. The routing is therefore the single most consequential piece of information on the card, but it is currently rendered at the lowest visual rank: text-xs text-gray-500, below the description, tied with the timestamp (resources/views/livewire/articles.blade.php:80-88).

The result is a misidentification risk. A user scanning headlines can form a wrong belief about which feed/channel a row belongs to and approve on that belief. The system then does exactly what it was told — the article publishes to the wrong channel, with no error raised anywhere. There is no validation that can catch this, because nothing is technically wrong.

Filtering alone does not solve this: with no filter applied, the default view remains just as ambiguous. The fix is hierarchy first, filtering second.

Current state

app/Livewire/Articles.php:

  • $tabpending or all, filters on approval_status
  • $search — matches article.title/description, only applied when $tab !== 'pending' (line 72, in the elseif) — so search is effectively dead on the pending tab
  • clear() (lines 50-54) rejects every pending row globally, ignoring all filters
  • The render query already eager-loads ['article.feed', 'feed', 'platformChannel'], so no new queries are needed for display

feeds table has no colour column and no settings key for one.

Scope — four parts, in order

1. Promote the route line

Restructure the card in articles.blade.php:

  • Move Feed → Channel above the headline as an eyebrow
  • text-xs font-medium, feed name text-gray-900, channel text-gray-600
  • Timestamp moves to its own muted line so it stops competing
  • Keep the existing glyph

No schema, no new data. This is the change that addresses the risk; the rest is refinement.

2. Feed colour as reinforcement

  • Migration: feeds.color, nullable string storing a palette key (slate, amber, …), not a hex — keeps Tailwind classes statically analysable
  • Palette map (enum or config), capped at ~8 hues
  • Feed::colorClasses() accessor with deterministic fallback (id % palette size) so existing rows get stable colours without a backfill
  • Colour dot before the feed name in the eyebrow

Colour is never the sole identifier — the feed name is always present. Beyond ~8 feeds hues stop being distinguishable, and colour-only encoding fails for colour-blind users.

3. Group pending rows by feed

  • When $tab === 'pending', group the paginated collection by feed_id for display
  • Feed heading per group, with colour dot and count
  • Chronological order preserved within each group

Open decision: a group can straddle a page boundary. Simplest approach is to group only what is on the current page and accept a feed appearing on two pages. Alternative is paginating by feed, which is a larger change. Decide during implementation.

4. Feed filter

  • public ?int $feedId = null
  • updatedFeedId()resetPage(), matching updatedSearch()
  • $query->where('feed_id', $this->feedId) when set
  • Options populated from feeds that actually have route articles
  • Reset alongside $search in setTab()
  • Applies on both tabs — unlike $search. Filtering by feed while reviewing pending is the primary use case. This resolves the open question from the original ticket.

5. Fix clear() scoping

clear() currently rejects all pending rows regardless of active filters. Once a feed filter exists, a user who has filtered to one feed and clicks "clear" will reasonably expect it to affect only that feed — and will instead wipe the entire queue. Irreversible in one click.

  • Scope clear() to the active $feedId filter
  • Add a confirmation step regardless of scoping

Acceptance criteria

  • Feed → Channel renders above the headline at readable weight
  • Each feed has a stable colour; existing feeds get one without a backfill
  • Colour is never the only signal — feed name always present
  • Pending rows group under feed headings
  • Feed select renders and filters the list
  • Filter applies on both pending and all tabs
  • Filter resets pagination on change
  • Filter combines correctly with tab and search
  • "All feeds" clears the filter
  • clear() respects the active feed filter and asks for confirmation
  • Feature tests: filtering, combining with search, combining with tab, reset behaviour, and clear() scoping

Commit sequence

  1. Promote route line in card hierarchy
  2. Add feed colour column, palette, and dot
  3. Group pending articles by feed
  4. Add feed filter
  5. Scope clear() to active filter + confirm
## Summary Every row on the Articles page is a `RouteArticle` — a feed × channel pairing. Approving one publishes to that row's channel. The routing is therefore the single most consequential piece of information on the card, but it is currently rendered at the *lowest* visual rank: `text-xs text-gray-500`, below the description, tied with the timestamp (`resources/views/livewire/articles.blade.php:80-88`). The result is a misidentification risk. A user scanning headlines can form a wrong belief about which feed/channel a row belongs to and approve on that belief. The system then does exactly what it was told — the article publishes to the wrong channel, with no error raised anywhere. There is no validation that can catch this, because nothing is technically wrong. Filtering alone does not solve this: with no filter applied, the default view remains just as ambiguous. The fix is hierarchy first, filtering second. ## Current state `app/Livewire/Articles.php`: - `$tab` — `pending` or `all`, filters on `approval_status` - `$search` — matches `article.title`/`description`, **only applied when `$tab !== 'pending'`** (line 72, in the `elseif`) — so search is effectively dead on the pending tab - `clear()` (lines 50-54) rejects **every** pending row globally, ignoring all filters - The render query already eager-loads `['article.feed', 'feed', 'platformChannel']`, so no new queries are needed for display `feeds` table has no colour column and no `settings` key for one. ## Scope — four parts, in order ### 1. Promote the route line Restructure the card in `articles.blade.php`: - Move `Feed → Channel` **above** the headline as an eyebrow - `text-xs font-medium`, feed name `text-gray-900`, channel `text-gray-600` - Timestamp moves to its own muted line so it stops competing - Keep the existing `→` glyph No schema, no new data. This is the change that addresses the risk; the rest is refinement. ### 2. Feed colour as reinforcement - Migration: `feeds.color`, nullable string storing a **palette key** (`slate`, `amber`, …), not a hex — keeps Tailwind classes statically analysable - Palette map (enum or config), capped at ~8 hues - `Feed::colorClasses()` accessor with deterministic fallback (`id % palette size`) so existing rows get stable colours without a backfill - Colour dot before the feed name in the eyebrow Colour is never the sole identifier — the feed name is always present. Beyond ~8 feeds hues stop being distinguishable, and colour-only encoding fails for colour-blind users. ### 3. Group pending rows by feed - When `$tab === 'pending'`, group the paginated collection by `feed_id` for display - Feed heading per group, with colour dot and count - Chronological order preserved within each group **Open decision**: a group can straddle a page boundary. Simplest approach is to group only what is on the current page and accept a feed appearing on two pages. Alternative is paginating by feed, which is a larger change. Decide during implementation. ### 4. Feed filter - `public ?int $feedId = null` - `updatedFeedId()` → `resetPage()`, matching `updatedSearch()` - `$query->where('feed_id', $this->feedId)` when set - Options populated from feeds that actually have route articles - Reset alongside `$search` in `setTab()` - **Applies on both tabs** — unlike `$search`. Filtering by feed while reviewing pending is the primary use case. This resolves the open question from the original ticket. ### 5. Fix `clear()` scoping `clear()` currently rejects all pending rows regardless of active filters. Once a feed filter exists, a user who has filtered to one feed and clicks "clear" will reasonably expect it to affect only that feed — and will instead wipe the entire queue. Irreversible in one click. - Scope `clear()` to the active `$feedId` filter - Add a confirmation step regardless of scoping ## Acceptance criteria - [ ] Feed → Channel renders above the headline at readable weight - [ ] Each feed has a stable colour; existing feeds get one without a backfill - [ ] Colour is never the only signal — feed name always present - [ ] Pending rows group under feed headings - [ ] Feed select renders and filters the list - [ ] Filter applies on both pending and all tabs - [ ] Filter resets pagination on change - [ ] Filter combines correctly with tab and search - [ ] "All feeds" clears the filter - [ ] `clear()` respects the active feed filter and asks for confirmation - [ ] Feature tests: filtering, combining with search, combining with tab, reset behaviour, and `clear()` scoping ## Commit sequence 1. Promote route line in card hierarchy 2. Add feed colour column, palette, and dot 3. Group pending articles by feed 4. Add feed filter 5. Scope `clear()` to active filter + confirm
myrmidex added this to the v1.4.0 milestone 2026-08-01 08:47:04 +02:00
myrmidex added the
enhancement
label 2026-08-01 08:47:04 +02:00
myrmidex self-assigned this 2026-08-01 08:47:04 +02:00
myrmidex changed title from Add feed filter to Articles page to Make article routing legible on the Articles page 2026-08-06 22:58:35 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lvl0/fedi-feed-router#113
No description provided.