Skip to content

Commit 9137eca

Browse files
authored
Merge pull request #52 from saagpatel/codex/refactor/wave5-5-ticket-workspace-rail
refactor(features): extract IntakeFieldControl + performance helpers from Rail
2 parents 4752f6f + 0631f49 commit 9137eca

4 files changed

Lines changed: 180 additions & 109 deletions

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// @vitest-environment jsdom
2+
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
3+
import { afterEach, describe, expect, it, vi } from "vitest";
4+
import { IntakeFieldControl } from "./IntakeFieldControl";
5+
6+
describe("IntakeFieldControl", () => {
7+
afterEach(() => cleanup());
8+
9+
it("renders a single-line text input when no rows prop is provided", () => {
10+
render(
11+
<IntakeFieldControl
12+
label="Issue summary"
13+
value="VPN outage in west region"
14+
onChange={vi.fn()}
15+
/>,
16+
);
17+
18+
const input = screen.getByLabelText("Issue summary") as HTMLInputElement;
19+
expect(input.tagName).toBe("INPUT");
20+
expect(input.type).toBe("text");
21+
expect(input.value).toBe("VPN outage in west region");
22+
});
23+
24+
it("renders a multi-line textarea when rows > 1 and reports new values through onChange", () => {
25+
const onChange = vi.fn();
26+
render(
27+
<IntakeFieldControl
28+
label="Symptoms"
29+
value="initial"
30+
rows={3}
31+
onChange={onChange}
32+
/>,
33+
);
34+
35+
const textarea = screen.getByLabelText("Symptoms") as HTMLTextAreaElement;
36+
expect(textarea.tagName).toBe("TEXTAREA");
37+
expect(textarea.rows).toBe(3);
38+
39+
fireEvent.change(textarea, { target: { value: "updated symptoms" } });
40+
expect(onChange).toHaveBeenCalledWith("updated symptoms");
41+
});
42+
43+
it("fires onChange with the latest value on single-line edits", () => {
44+
const onChange = vi.fn();
45+
render(
46+
<IntakeFieldControl label="Affected user" value="" onChange={onChange} />,
47+
);
48+
49+
fireEvent.change(screen.getByLabelText("Affected user"), {
50+
target: { value: "alice@example.com" },
51+
});
52+
expect(onChange).toHaveBeenCalledWith("alice@example.com");
53+
});
54+
});
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import type { NoteAudience } from "../../types/workspace";
2+
3+
export type IntakeField =
4+
| "issue"
5+
| "environment"
6+
| "impact"
7+
| "affected_user"
8+
| "affected_system"
9+
| "affected_site"
10+
| "symptoms"
11+
| "steps_tried"
12+
| "blockers"
13+
| "likely_category";
14+
15+
export const NOTE_AUDIENCES: Array<{ id: NoteAudience; label: string }> = [
16+
{ id: "internal-note", label: "Internal note" },
17+
{ id: "customer-safe", label: "Customer-safe" },
18+
{ id: "escalation-note", label: "Escalation note" },
19+
];
20+
21+
export const INTAKE_FIELDS: Array<{
22+
key: IntakeField;
23+
label: string;
24+
rows?: number;
25+
}> = [
26+
{ key: "issue", label: "Issue summary" },
27+
{ key: "environment", label: "Environment" },
28+
{ key: "impact", label: "Impact", rows: 2 },
29+
{ key: "affected_user", label: "Affected user" },
30+
{ key: "affected_system", label: "Affected system" },
31+
{ key: "affected_site", label: "Affected site" },
32+
{ key: "symptoms", label: "Symptoms", rows: 3 },
33+
{ key: "steps_tried", label: "Steps already tried", rows: 3 },
34+
{ key: "blockers", label: "Current blocker", rows: 2 },
35+
{ key: "likely_category", label: "Likely category" },
36+
];
37+
38+
interface IntakeFieldControlProps {
39+
label: string;
40+
value: string;
41+
rows?: number;
42+
onChange: (value: string) => void;
43+
}
44+
45+
export function IntakeFieldControl({
46+
label,
47+
value,
48+
rows,
49+
onChange,
50+
}: IntakeFieldControlProps) {
51+
if (rows && rows > 1) {
52+
return (
53+
<label className="ticket-workspace-rail__field">
54+
<span>{label}</span>
55+
<textarea
56+
rows={rows}
57+
value={value}
58+
onChange={(event) => onChange(event.target.value)}
59+
/>
60+
</label>
61+
);
62+
}
63+
64+
return (
65+
<label className="ticket-workspace-rail__field">
66+
<span>{label}</span>
67+
<input
68+
type="text"
69+
value={value}
70+
onChange={(event) => onChange(event.target.value)}
71+
/>
72+
</label>
73+
);
74+
}

src/features/workspace/TicketWorkspaceRail.tsx

Lines changed: 8 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -16,32 +16,15 @@ import type {
1616
WorkspaceFavorite,
1717
WorkspacePersonalization,
1818
} from "../../types/workspace";
19+
import { markWorkspaceReady } from "./workspacePerformanceMetrics";
20+
import {
21+
INTAKE_FIELDS,
22+
IntakeFieldControl,
23+
NOTE_AUDIENCES,
24+
type IntakeField,
25+
} from "./IntakeFieldControl";
1926
import "./TicketWorkspaceRail.css";
2027

21-
const APP_BOOTSTRAP_START_MARK = "assistsupport:perf:bootstrap-start";
22-
const TICKET_WORKSPACE_READY_MARK = "assistsupport:perf:ticket-workspace-ready";
23-
const TICKET_WORKSPACE_READY_MEASURE =
24-
"assistsupport:perf:ticket-workspace-ready-ms";
25-
26-
type PerfWindow = Window & {
27-
__assistsupportPerf?: {
28-
bootstrapStartedAt: number;
29-
ticketWorkspaceReadyMs?: number;
30-
};
31-
};
32-
33-
type IntakeField =
34-
| "issue"
35-
| "environment"
36-
| "impact"
37-
| "affected_user"
38-
| "affected_system"
39-
| "affected_site"
40-
| "symptoms"
41-
| "steps_tried"
42-
| "blockers"
43-
| "likely_category";
44-
4528
interface TicketWorkspaceRailProps {
4629
intake: CaseIntake;
4730
onIntakeChange: (field: IntakeField, value: string) => void;
@@ -90,62 +73,6 @@ interface TicketWorkspaceRailProps {
9073
currentResponse: string;
9174
}
9275

93-
const NOTE_AUDIENCES: Array<{ id: NoteAudience; label: string }> = [
94-
{ id: "internal-note", label: "Internal note" },
95-
{ id: "customer-safe", label: "Customer-safe" },
96-
{ id: "escalation-note", label: "Escalation note" },
97-
];
98-
99-
const INTAKE_FIELDS: Array<{ key: IntakeField; label: string; rows?: number }> =
100-
[
101-
{ key: "issue", label: "Issue summary" },
102-
{ key: "environment", label: "Environment" },
103-
{ key: "impact", label: "Impact", rows: 2 },
104-
{ key: "affected_user", label: "Affected user" },
105-
{ key: "affected_system", label: "Affected system" },
106-
{ key: "affected_site", label: "Affected site" },
107-
{ key: "symptoms", label: "Symptoms", rows: 3 },
108-
{ key: "steps_tried", label: "Steps already tried", rows: 3 },
109-
{ key: "blockers", label: "Current blocker", rows: 2 },
110-
{ key: "likely_category", label: "Likely category" },
111-
];
112-
113-
function IntakeFieldControl({
114-
label,
115-
value,
116-
rows,
117-
onChange,
118-
}: {
119-
label: string;
120-
value: string;
121-
rows?: number;
122-
onChange: (value: string) => void;
123-
}) {
124-
if (rows && rows > 1) {
125-
return (
126-
<label className="ticket-workspace-rail__field">
127-
<span>{label}</span>
128-
<textarea
129-
rows={rows}
130-
value={value}
131-
onChange={(event) => onChange(event.target.value)}
132-
/>
133-
</label>
134-
);
135-
}
136-
137-
return (
138-
<label className="ticket-workspace-rail__field">
139-
<span>{label}</span>
140-
<input
141-
type="text"
142-
value={value}
143-
onChange={(event) => onChange(event.target.value)}
144-
/>
145-
</label>
146-
);
147-
}
148-
14976
export function TicketWorkspaceRail({
15077
intake,
15178
onIntakeChange,
@@ -209,35 +136,7 @@ export function TicketWorkspaceRail({
209136
}, [guidedRunbookSession]);
210137

211138
useEffect(() => {
212-
if (
213-
typeof window === "undefined" ||
214-
typeof window.performance === "undefined"
215-
) {
216-
return;
217-
}
218-
219-
const perfWindow = window as PerfWindow;
220-
const bootstrapStartedAt =
221-
perfWindow.__assistsupportPerf?.bootstrapStartedAt ?? 0;
222-
perfWindow.__assistsupportPerf = {
223-
...(perfWindow.__assistsupportPerf ?? { bootstrapStartedAt }),
224-
bootstrapStartedAt,
225-
ticketWorkspaceReadyMs: Number(
226-
(window.performance.now() - bootstrapStartedAt).toFixed(2),
227-
),
228-
};
229-
window.performance.clearMarks(TICKET_WORKSPACE_READY_MARK);
230-
window.performance.clearMeasures(TICKET_WORKSPACE_READY_MEASURE);
231-
window.performance.mark(TICKET_WORKSPACE_READY_MARK);
232-
try {
233-
window.performance.measure(
234-
TICKET_WORKSPACE_READY_MEASURE,
235-
APP_BOOTSTRAP_START_MARK,
236-
TICKET_WORKSPACE_READY_MARK,
237-
);
238-
} catch {
239-
// Keep the workspace usable even if performance marks are unavailable in the current runtime.
240-
}
139+
markWorkspaceReady();
241140
}, []);
242141

243142
return (
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
export const APP_BOOTSTRAP_START_MARK = "assistsupport:perf:bootstrap-start";
2+
export const TICKET_WORKSPACE_READY_MARK =
3+
"assistsupport:perf:ticket-workspace-ready";
4+
export const TICKET_WORKSPACE_READY_MEASURE =
5+
"assistsupport:perf:ticket-workspace-ready-ms";
6+
7+
type PerfWindow = Window & {
8+
__assistsupportPerf?: {
9+
bootstrapStartedAt: number;
10+
ticketWorkspaceReadyMs?: number;
11+
};
12+
};
13+
14+
export function markWorkspaceReady(): void {
15+
if (
16+
typeof window === "undefined" ||
17+
typeof window.performance === "undefined"
18+
) {
19+
return;
20+
}
21+
22+
const perfWindow = window as PerfWindow;
23+
const bootstrapStartedAt =
24+
perfWindow.__assistsupportPerf?.bootstrapStartedAt ?? 0;
25+
perfWindow.__assistsupportPerf = {
26+
...(perfWindow.__assistsupportPerf ?? { bootstrapStartedAt }),
27+
bootstrapStartedAt,
28+
ticketWorkspaceReadyMs: Number(
29+
(window.performance.now() - bootstrapStartedAt).toFixed(2),
30+
),
31+
};
32+
window.performance.clearMarks(TICKET_WORKSPACE_READY_MARK);
33+
window.performance.clearMeasures(TICKET_WORKSPACE_READY_MEASURE);
34+
window.performance.mark(TICKET_WORKSPACE_READY_MARK);
35+
try {
36+
window.performance.measure(
37+
TICKET_WORKSPACE_READY_MEASURE,
38+
APP_BOOTSTRAP_START_MARK,
39+
TICKET_WORKSPACE_READY_MARK,
40+
);
41+
} catch {
42+
// Keep the workspace usable even if performance marks are unavailable in the current runtime.
43+
}
44+
}

0 commit comments

Comments
 (0)