diff --git a/frontend/src/routes/$incidentId/components/ActionItemsList.test.tsx b/frontend/src/routes/$incidentId/components/ActionItemsList.test.tsx
index e6fe87dd..e4b6b4f4 100644
--- a/frontend/src/routes/$incidentId/components/ActionItemsList.test.tsx
+++ b/frontend/src/routes/$incidentId/components/ActionItemsList.test.tsx
@@ -1,5 +1,6 @@
import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
import {render, screen, waitFor} from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
import {TooltipProvider} from 'components/Tooltip';
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
@@ -151,4 +152,49 @@ describe('ActionItemsList', () => {
expect(screen.getByRole('button', {name: 'Sync action items'})).toBeInTheDocument();
expect(screen.getByRole('button', {name: 'Create action item'})).toBeInTheDocument();
});
+
+ it('opens Linear create-sub-issue URL when parent issue ID is available', async () => {
+ mockApiGet.mockResolvedValue([]);
+ const windowOpenSpy = vi.spyOn(window, 'open').mockImplementation(() => null);
+
+ renderWithProviders(
+
+ );
+
+ await userEvent.click(screen.getByRole('button', {name: 'Create action item'}));
+ await userEvent.click(screen.getByRole('button', {name: 'Create in Linear'}));
+
+ expect(windowOpenSpy).toHaveBeenCalledWith(
+ 'https://linear.app/myworkspace/new?parentId=abc-123-def',
+ '_blank',
+ 'noopener,noreferrer'
+ );
+ windowOpenSpy.mockRestore();
+ });
+
+ it('falls back to parent issue URL when parent issue ID is not available', async () => {
+ mockApiGet.mockResolvedValue([]);
+ const windowOpenSpy = vi.spyOn(window, 'open').mockImplementation(() => null);
+
+ renderWithProviders(
+
+ );
+
+ await userEvent.click(screen.getByRole('button', {name: 'Create action item'}));
+ await userEvent.click(screen.getByRole('button', {name: 'Create in Linear'}));
+
+ expect(windowOpenSpy).toHaveBeenCalledWith(
+ 'https://linear.app/myworkspace/issue/INC-1',
+ '_blank',
+ 'noopener,noreferrer'
+ );
+ windowOpenSpy.mockRestore();
+ });
});
diff --git a/frontend/src/routes/$incidentId/components/ActionItemsList.tsx b/frontend/src/routes/$incidentId/components/ActionItemsList.tsx
index 19a93446..a78620c1 100644
--- a/frontend/src/routes/$incidentId/components/ActionItemsList.tsx
+++ b/frontend/src/routes/$incidentId/components/ActionItemsList.tsx
@@ -62,12 +62,28 @@ function ActionItemCard({item}: {item: ActionItem}) {
);
}
+function buildLinearCreateUrl(linearUrl: string, parentIssueId: string): string | null {
+ try {
+ const url = new URL(linearUrl);
+ const workspace = url.pathname.split('/')[1];
+ if (!workspace) return null;
+ return `https://linear.app/${workspace}/new?parentId=${parentIssueId}`;
+ } catch {
+ return null;
+ }
+}
+
interface ActionItemsListProps {
incidentId: string;
linearUrl?: string;
+ linearParentIssueId?: string | null;
}
-export function ActionItemsList({incidentId, linearUrl}: ActionItemsListProps) {
+export function ActionItemsList({
+ incidentId,
+ linearUrl,
+ linearParentIssueId,
+}: ActionItemsListProps) {
const queryClient = useQueryClient();
const syncMutation = useMutation(
syncActionItemsMutationOptions(queryClient, incidentId)
@@ -127,8 +143,8 @@ export function ActionItemsList({incidentId, linearUrl}: ActionItemsListProps) {
title="Create Action Item"
message={
<>
- You'll be taken to the parent Linear issue for the incident. Create your
- action item as a sub-issue or related issue there.
+ You'll be taken to Linear to create a sub-issue under the parent incident
+ issue.
Make sure to assign the issue to the appropriate team as issues in the
@@ -154,9 +170,11 @@ export function ActionItemsList({incidentId, linearUrl}: ActionItemsListProps) {
>
}
- confirmLabel="Open Linear"
+ confirmLabel="Create in Linear"
onConfirm={() => {
- window.open(linearUrl, '_blank', 'noopener,noreferrer');
+ const createUrl =
+ linearParentIssueId && buildLinearCreateUrl(linearUrl, linearParentIssueId);
+ window.open(createUrl || linearUrl, '_blank', 'noopener,noreferrer');
setShowCreateDialog(false);
}}
onCancel={() => setShowCreateDialog(false)}
diff --git a/frontend/src/routes/$incidentId/index.tsx b/frontend/src/routes/$incidentId/index.tsx
index 47a17280..60678fc7 100644
--- a/frontend/src/routes/$incidentId/index.tsx
+++ b/frontend/src/routes/$incidentId/index.tsx
@@ -66,6 +66,7 @@ function Incident() {
diff --git a/frontend/src/routes/$incidentId/queries/incidentDetailQueryOptions.ts b/frontend/src/routes/$incidentId/queries/incidentDetailQueryOptions.ts
index bb094c60..ac2c5f9d 100644
--- a/frontend/src/routes/$incidentId/queries/incidentDetailQueryOptions.ts
+++ b/frontend/src/routes/$incidentId/queries/incidentDetailQueryOptions.ts
@@ -39,6 +39,7 @@ const IncidentDetailSchema = z.object({
impact_type_tags: z.array(z.string()),
participants: z.array(ParticipantSchema),
external_links: ExternalLinksSchema,
+ linear_parent_issue_id: z.string().nullable(),
time_started: z.string().nullable(),
time_detected: z.string().nullable(),
time_analyzed: z.string().nullable(),
diff --git a/src/firetower/incidents/serializers.py b/src/firetower/incidents/serializers.py
index de16ee16..4472ec7e 100644
--- a/src/firetower/incidents/serializers.py
+++ b/src/firetower/incidents/serializers.py
@@ -157,6 +157,7 @@ class Meta:
"root_cause_tags",
"impact_type_tags",
"external_links",
+ "linear_parent_issue_id",
"created_at",
"updated_at",
"time_started",
@@ -168,6 +169,7 @@ class Meta:
]
read_only_fields = [
"id",
+ "linear_parent_issue_id",
"created_at",
"updated_at",
"time_started",
diff --git a/src/firetower/incidents/tests/test_serializers.py b/src/firetower/incidents/tests/test_serializers.py
index 13554f38..1f4ca32f 100644
--- a/src/firetower/incidents/tests/test_serializers.py
+++ b/src/firetower/incidents/tests/test_serializers.py
@@ -148,6 +148,9 @@ def test_incident_detail_serialization(self):
assert "linear" not in data["external_links"] # Not set, so not included
assert len(data["external_links"]) == 1
+ # Check linear_parent_issue_id is exposed (null when not set)
+ assert data["linear_parent_issue_id"] is None
+
@pytest.mark.django_db
class TestIncidentWriteSerializerHooks: