The Unassigned queue stops offering merged tickets as work (#1054)
The count and the list were the same question spelled two ways in one method: the sidebar counts scope by $closedSlugs, read from ticketing_statuses, while the `unassigned` and `mine` list branches carried a hand-written ["resolved", "closed"] that predates the `merged` status. So a correctly merged ticket — status merged, merged_into_id set, its own Show page already calling it read-only — stayed in the queue agents pull new work from, and the page contradicted itself on screen: "Unassigned 4" beside five rendered rows. Both queues now scope by $closedSlugs, so they exclude exactly what their own counts exclude. The scoping is by STATUS and not by merged_into_id on purpose: #938 rows (pointer set, status still open) are live, editable work until a human repairs them, and dropping them out of the queue would hide the rows that most need one. There is a NEGCTRL test for that. Also, the secondary finding: ?filter=closed was accepted, silently returned the All-Open set and was echoed back in props.filters as though a Closed view existed. The sidebar has six views and that is not one of them, so an unrecognised filter normalises to `all` and says so. The list is now a constant that savePrefs() validates against too, rather than the same six slugs written out twice. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,13 @@ use Inertia\Response;
|
|||||||
|
|
||||||
class TicketController extends Controller
|
class TicketController extends Controller
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* The sidebar's views, and the only values `?filter=` means anything as.
|
||||||
|
* Index.vue's `visibleViews` draws exactly these; `savePrefs()` validates
|
||||||
|
* against them; `index()` normalises anything else to `all` (#1054).
|
||||||
|
*/
|
||||||
|
private const VIEW_FILTERS = ['all', 'mine', 'unassigned', 'pending', 'resolved', 'merged'];
|
||||||
|
|
||||||
private function agentGroupIds(): array
|
private function agentGroupIds(): array
|
||||||
{
|
{
|
||||||
return TicketingAgentAccess::where('user_id', Auth::id())
|
return TicketingAgentAccess::where('user_id', Auth::id())
|
||||||
@@ -112,6 +119,18 @@ class TicketController extends Controller
|
|||||||
$request->merge(['filter' => $pref['filter']]);
|
$request->merge(['filter' => $pref['filter']]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #1054 — a `filter` the sidebar has no view for used to fall through to
|
||||||
|
// the All Open branch and then be echoed straight back in `props.filters`,
|
||||||
|
// so `?filter=closed` looked like a real view that just happened to be
|
||||||
|
// showing open tickets. There is no Closed view anywhere in the UI;
|
||||||
|
// anything unrecognised is All Open, and now says so. The list is the
|
||||||
|
// one `savePrefs()` validates against and the one Index.vue draws.
|
||||||
|
// Only rewrites a filter that was actually asked for: an absent one keeps
|
||||||
|
// meaning "no view chosen", which is what leaves `filter` out of the URL.
|
||||||
|
if ($request->filled('filter') && ! in_array($request->filter, self::VIEW_FILTERS, true)) {
|
||||||
|
$request->merge(['filter' => 'all']);
|
||||||
|
}
|
||||||
|
|
||||||
// If no groups exist at all, render a first-run / bootstrap state
|
// If no groups exist at all, render a first-run / bootstrap state
|
||||||
$totalGroups = TicketingGroup::count();
|
$totalGroups = TicketingGroup::count();
|
||||||
if ($totalGroups === 0) {
|
if ($totalGroups === 0) {
|
||||||
@@ -181,10 +200,20 @@ class TicketController extends Controller
|
|||||||
if ($request->filled('project_id')) {
|
if ($request->filled('project_id')) {
|
||||||
$query->where('project_id', $request->project_id);
|
$query->where('project_id', $request->project_id);
|
||||||
}
|
}
|
||||||
|
// #1054 — the work queues scope by `$closedSlugs`, the same list the
|
||||||
|
// counts above use. They used to spell it `['resolved', 'closed']`
|
||||||
|
// by hand, which predates the `merged` status: a correctly merged
|
||||||
|
// ticket stayed in Unassigned as pickable work while the sidebar
|
||||||
|
// count next to it — already reading `$closedSlugs` — excluded it,
|
||||||
|
// so the view disagreed with its own header (4 vs 5). Scoping by
|
||||||
|
// STATUS and not by `merged_into_id` is deliberate: #938's
|
||||||
|
// inconsistent rows (pointer set, status still open) are live work
|
||||||
|
// until somebody repairs them, and hiding them from the queue would
|
||||||
|
// hide the rows that most need a human.
|
||||||
if ($request->filter === 'mine') {
|
if ($request->filter === 'mine') {
|
||||||
$query->where('assigned_to', $user->id)->whereNotIn('status', ['resolved', 'closed']);
|
$query->where('assigned_to', $user->id)->whereNotIn('status', $closedSlugs);
|
||||||
} elseif ($request->filter === 'unassigned') {
|
} elseif ($request->filter === 'unassigned') {
|
||||||
$query->whereNull('assigned_to')->whereNotIn('status', ['resolved', 'closed']);
|
$query->whereNull('assigned_to')->whereNotIn('status', $closedSlugs);
|
||||||
} elseif ($request->filter === 'pending') {
|
} elseif ($request->filter === 'pending') {
|
||||||
$query->where('status', 'pending');
|
$query->where('status', 'pending');
|
||||||
} elseif ($request->filter === 'resolved') {
|
} elseif ($request->filter === 'resolved') {
|
||||||
@@ -630,7 +659,7 @@ class TicketController extends Controller
|
|||||||
{
|
{
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'view_mode' => 'sometimes|in:list,kanban',
|
'view_mode' => 'sometimes|in:list,kanban',
|
||||||
'filter' => 'sometimes|in:all,mine,unassigned,pending,resolved,merged',
|
'filter' => 'sometimes|in:'.implode(',', self::VIEW_FILTERS),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (empty($validated)) {
|
if (empty($validated)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user