Issue Types
View on GitHubExtend required fields
An issue type's required_fields is a fixed set of keys, each mapped
to the actual Issue create/update request field it enforces
non-emptiness on. Worked example: adding a parent option, so a
project can mark a type (say, a Sub-task-shaped custom type) as
"must always be created as a sub-issue of something."
Step 1 — Add the mapping
File: app/Services/IssueTypeService.php
/**
* The fixed set of issue fields that an issue type can mark as required,
* mapped to the request/data key each one corresponds to on Issue
* create/update.
*/
public const array REQUIRED_FIELD_TO_DATA_KEY = [
'description' => 'description',
'assignee' => 'assignee_id',
'labels' => 'labels',
'start_date' => 'start_date',
'end_date' => 'end_date',
'priority' => 'priority',
'parent' => 'parent_id',
];
assertRequiredFieldsSatisfied() (same file) already does the actual
enforcement generically off this map — nothing else in that method
needs to change:
public function assertRequiredFieldsSatisfied(IssueType $issueType, array $data, bool $isCreate): void
{
$missingDataKeys = [];
foreach ($issueType->required_fields ?? [] as $field) {
$dataKey = self::REQUIRED_FIELD_TO_DATA_KEY[$field] ?? null;
if (! $dataKey) {
continue;
}
if ($isCreate) {
if ($this->isEmptyValue($data[$dataKey] ?? null)) {
$missingDataKeys[$dataKey] = true;
}
} elseif (array_key_exists($dataKey, $data) && $this->isEmptyValue($data[$dataKey])) {
$missingDataKeys[$dataKey] = true;
}
}
if ($missingDataKeys) {
throw ValidationException::withMessages(array_fill_keys(
array_keys($missingDataKeys),
'This field is required for the selected issue type.',
));
}
}
IssueController::store/update already call this method (after
resolving the issue's type and merging in parent_id/issue_type_id
changes), and it's already wired into the request lifecycle — see
app/Http/Controllers/IssueController.php's store() method, which
calls $this->issueTypeService->assertRequiredFieldsSatisfied($issueType, $data, isCreate: true);
right before $this->issueService->createIssue($data). Because
parent_id is already a real field on the create/update request (see
../architecture/02-backend-layered-architecture.mdArchitecture & project technicalsBackend layered architectureController → Service → Repository, with no exceptions and no shortcuts — a controller never builds an Eloquent query, and a repository never contains busin…
for the general request-validation shape), no controller change is
needed here at all — the new map entry is enough.
Step 2 — Allow it through the settings validation
File: app/Http/Controllers/IssueTypeController.php
Both store() and update() validate required_fields.* against
Rule::in(array_keys(IssueTypeService::REQUIRED_FIELD_TO_DATA_KEY)) —
since that reads the map's keys directly, adding 'parent' to the map
in Step 1 is enough for the settings endpoints to accept it too. No
change needed here, but it's worth knowing this is why Step 1 alone
is sufficient:
$validated = $request->validate([
'name' => ['required', 'string', 'max:50'],
'icon' => ['required', 'string', 'max:50'],
'color' => ['required', 'string', 'regex:/^#[0-9a-fA-F]{6}$/'],
'description' => ['nullable', 'string', 'max:255'],
'allows_children' => ['sometimes', 'boolean'],
'required_fields' => ['sometimes', 'array'],
'required_fields.*' => ['string', Rule::in(array_keys(IssueTypeService::REQUIRED_FIELD_TO_DATA_KEY))],
'restricted_role_types' => ['sometimes', 'array'],
'restricted_role_types.*' => ['string', Rule::in(array_map(fn (RoleType $role) => $role->value, [RoleType::OWNER, RoleType::ADMIN, RoleType::MEMBER, RoleType::VIEWER]))],
]);
Step 3 — Add the checkbox client-side
File: resources/js/Components/Organisms/WorkspaceSettingsContent/WorkspaceSettingsIssueTypeInlineEditor.tsx
const REQUIRED_FIELD_OPTIONS: { value: string; label: string }[] = [
{ value: 'description', label: 'Description' },
{ value: 'assignee', label: 'Assignee' },
{ value: 'labels', label: 'Labels' },
{ value: 'start_date', label: 'Start date' },
{ value: 'end_date', label: 'End date' },
{ value: 'priority', label: 'Priority' },
{ value: 'parent', label: 'Parent issue' },
];
This array drives the checkbox grid under "Required fields" in the inline editor directly — nothing else in that component reads a fixed list of field names, so this is the only frontend change needed.
Step 4 — Tests
File: tests/Feature/IssueTypeWorkflowIntegrationTest.php
Mirror the existing 'creating an issue of a type with required fields rejects a request missing one' test:
test('creating an issue of a type requiring a parent rejects a request without one', function () {
$project = Project::factory()->create();
$member = User::factory()->create();
$project->users()->attach($member->id, ['role' => 'member']);
$this->actingAs($member)->post('/issues', ['title' => 'seed', 'project_id' => $project->id, 'priority' => 'low', 'status' => 'open']);
$subtaskType = $project->issueTypes()->create(['name' => 'Sub-task', 'icon' => 'Bug', 'color' => '#000000', 'required_fields' => ['parent']]);
$response = $this->actingAs($member)->post('/issues', [
'title' => 'Orphaned sub-task',
'project_id' => $project->id,
'priority' => 'low',
'status' => 'open',
'issue_type_id' => $subtaskType->id,
]);
$response->assertSessionHasErrors('parent_id');
});
File: tests/Feature/IssueTypeControllerTest.php
Add 'parent' to the existing
'an admin can set required fields and restricted role types on an issue type' test's asserted array so the settings endpoint round-trip
is covered too.
Run php artisan test --filter=IssueTypeWorkflowIntegrationTest and
php artisan test --filter=IssueTypeControllerTest, plus
npx vitest run resources/js/Components/Organisms/WorkspaceSettingsContent/WorkspaceSettingsIssueTypeInlineEditor.test.tsx,
before committing.
