Priorities
View on GitHubChange the starter priorities
Worked example: adding a fourth starter priority, Urgent, above the
three every project is seeded with (High, Medium, Low). Editing
SYSTEM_PRIORITIES only changes what a brand-new project starts
with — a project whose scale was already seeded (it has a non-null
projects.priorities_seeded_at) keeps exactly the priorities it has now,
because ensureSystemPriorities() runs once per project and never
resurrects or tops up anything (see the architecture note in
README.md).
If you just want an extra priority in one project, don't touch any code — use Settings → Priorities in the app itself. This guide is for changing what every new project gets, and for bringing the projects that already exist along when you really want to.
Step 1 — Add the definition
File: app/Services/PriorityService.php
Urgent is the most important priority, so it takes position 0 and
the others shift down by one. The default stays Medium.
public const array SYSTEM_PRIORITIES = [
['name' => 'Urgent', 'color' => '#b71c1c', 'icon' => 'Siren', 'position' => 0, 'is_default' => false],
['name' => 'High', 'color' => '#f44336', 'icon' => 'SignalHigh', 'position' => 1, 'is_default' => false],
['name' => 'Medium', 'color' => '#ff9800', 'icon' => 'SignalMedium', 'position' => 2, 'is_default' => true],
['name' => 'Low', 'color' => '#4caf50', 'icon' => 'SignalLow', 'position' => 3, 'is_default' => false],
];
color must be a 6-digit hex string and icon a lucide-react icon
name. PriorityController@store/@update validate the color with
regex:/^#[0-9a-fA-F]{6}$/, but ensureSystemPriorities() skips that
validation entirely (it inserts these rows directly through
PriorityRepository::firstOrCreateSystemPriority()), so a malformed
value would only surface later as a broken swatch. Prefer an icon from
PRIORITY_ICON_OPTIONS in resources/js/utils/priorityIcons.ts
(Siren is in it): resolvePriorityIcon() falls back to Flag for a
name lucide doesn't know, but an icon outside the list is not offered by
the settings tab's icon picker.
That's it for new projects: ensureSystemPriorities() iterates the
list and calls firstOrCreateSystemPriority() for each entry, matching
on name, the next time an unseeded project's priorities are needed
(its first issue create/update, or opening its Settings → Priorities
tab). Nothing else keeps its own copy of the list — SYSTEM_PRIORITIES
is also what the legacy-data migrations seed from — so there is no
frontend list, enum or <select> to update.
Two behaviours to be aware of: position 0 is what the dashboard's
topPriority counter treats as "top of the scale", so new projects now
count Urgent issues there instead of High; and the Medium default
means new issues are still created as Medium.
Step 2 — Bring existing projects along (optional)
Skip this and every existing project simply keeps its three priorities,
which is usually right: those projects' owners may have renamed or
deleted High/Low on purpose. If you do want Urgent everywhere, ship
a data migration — never loop over projects in a service or a request.
It must only touch projects that have been seeded (an unseeded one
gets Urgent from Step 1 when it is seeded), be idempotent, leave
projects that already have a priority of that name alone, and bump the
issue cache because it writes through the query builder (no model event
runs).
File: database/migrations/2026_10_19_100000_add_urgent_priority_to_seeded_projects.php
<?php
use App\Services\PriorityService;
use App\Support\Cache\CacheVersion;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
/**
* Projects seeded before "Urgent" joined the starter scale get it at the top
* of their own scale (the others move down one place). A project that
* already has a priority called "Urgent" in any case is left alone, and an
* unseeded project is skipped: it receives the new starter scale when it is
* seeded.
*/
return new class extends Migration
{
public function up(): void
{
$urgent = collect(PriorityService::SYSTEM_PRIORITIES)->firstWhere('name', 'Urgent');
DB::table('projects')
->whereNotNull('priorities_seeded_at')
->pluck('id')
->each(function (int $projectId) use ($urgent) {
$exists = DB::table('priorities')
->where('project_id', $projectId)
->whereRaw('lower(name) = ?', [mb_strtolower($urgent['name'])])
->exists();
if ($exists) {
return;
}
DB::table('priorities')->where('project_id', $projectId)->increment('position');
DB::table('priorities')->insert([
'project_id' => $projectId,
'name' => $urgent['name'],
'color' => $urgent['color'],
'icon' => $urgent['icon'],
'position' => 0,
'is_default' => false,
'created_at' => now(),
'updated_at' => now(),
]);
});
// Written through the query builder, so no model event dropped the
// cached issue lists (they carry each issue's priority position).
CacheVersion::bump(CacheVersion::ISSUES);
}
public function down(): void
{
// Issues may already use the new priority; deleting it would orphan
// them silently, so rolling back is left to a human.
}
};
Like the real 2026_10_18_* migrations it reads the definition from
PriorityService::SYSTEM_PRIORITIES instead of repeating the values.
That couples the migration to the constant's current value, which is
fine for a one-off you run once; if the starter list changes again
later, this migration should not silently change meaning — copy the
literal values in if it has to stay replayable.
Step 3 — Update the tests that assert the starter scale
File: tests/Feature/PriorityServiceTest.php
test('a project is seeded with High, Medium and Low in that order, Medium being the default', function () {
$priorities = $this->service->getPriorities($this->project);
expect($priorities->pluck('name')->all())->toBe(['High', 'Medium', 'Low'])
->and($priorities->pluck('position')->all())->toBe([0, 1, 2])
->and($priorities->firstWhere('is_default', true)->name)->toBe('Medium')
->and($this->project->fresh()->priorities_seeded_at)->not->toBeNull();
});
becomes:
test('a project is seeded with Urgent, High, Medium and Low in that order, Medium being the default', function () {
$priorities = $this->service->getPriorities($this->project);
expect($priorities->pluck('name')->all())->toBe(['Urgent', 'High', 'Medium', 'Low'])
->and($priorities->pluck('position')->all())->toBe([0, 1, 2, 3])
->and($priorities->firstWhere('is_default', true)->name)->toBe('Medium')
->and($this->project->fresh()->priorities_seeded_at)->not->toBeNull();
});
Then run the whole PHP suite, not just that file — a lot of tests assume the exact starter scale. Expect to adjust:
- tests that create or rename a priority to
Urgent(e.g. inPriorityServiceTest,PriorityControllerTest,IssueControllerTest) — the name now exists in a seeded project and is refused as a duplicate, so give those tests another name (Critical); - tests that assert a count or position of the seeded scale, such as
->position3for a newly created priority (now4) orpriorities()->count()4(now5); - tests asserting the dashboard's
topPriority/prioritystats, if they rely onHighbeing position0.
Add one test for the migration, modelled on
tests/Feature/MoveIssuePriorityTest.php /
tests/Feature/MigratePriorityNameReferencesTest.php: create one project
with its three old priorities by hand and set priorities_seeded_at
(ensureSystemPriorities() would already seed Urgent), run the migration, and assert Urgent is at
position 0, the others shifted down by one, the default is unchanged,
running it twice does not add a second Urgent, and an unseeded project
gets no row.
The frontend has no copy of the starter scale to change — the Vitest
and Storybook fixtures (priorities in resources/js/stories/fixtures.ts)
are sample data, not a mirror of SYSTEM_PRIORITIES.
Run php artisan test --filter=Priority and then composer test
before committing.
