Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions src/features/workspace/IntakeFieldControl.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// @vitest-environment jsdom
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { IntakeFieldControl } from "./IntakeFieldControl";

describe("IntakeFieldControl", () => {
afterEach(() => cleanup());

it("renders a single-line text input when no rows prop is provided", () => {
render(
<IntakeFieldControl
label="Issue summary"
value="VPN outage in west region"
onChange={vi.fn()}
/>,
);

const input = screen.getByLabelText("Issue summary") as HTMLInputElement;
expect(input.tagName).toBe("INPUT");
expect(input.type).toBe("text");
expect(input.value).toBe("VPN outage in west region");
});

it("renders a multi-line textarea when rows > 1 and reports new values through onChange", () => {
const onChange = vi.fn();
render(
<IntakeFieldControl
label="Symptoms"
value="initial"
rows={3}
onChange={onChange}
/>,
);

const textarea = screen.getByLabelText("Symptoms") as HTMLTextAreaElement;
expect(textarea.tagName).toBe("TEXTAREA");
expect(textarea.rows).toBe(3);

fireEvent.change(textarea, { target: { value: "updated symptoms" } });
expect(onChange).toHaveBeenCalledWith("updated symptoms");
});

it("fires onChange with the latest value on single-line edits", () => {
const onChange = vi.fn();
render(
<IntakeFieldControl label="Affected user" value="" onChange={onChange} />,
);

fireEvent.change(screen.getByLabelText("Affected user"), {
target: { value: "alice@example.com" },
});
expect(onChange).toHaveBeenCalledWith("alice@example.com");
});
});
74 changes: 74 additions & 0 deletions src/features/workspace/IntakeFieldControl.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type { NoteAudience } from "../../types/workspace";

export type IntakeField =
| "issue"
| "environment"
| "impact"
| "affected_user"
| "affected_system"
| "affected_site"
| "symptoms"
| "steps_tried"
| "blockers"
| "likely_category";

export const NOTE_AUDIENCES: Array<{ id: NoteAudience; label: string }> = [
{ id: "internal-note", label: "Internal note" },
{ id: "customer-safe", label: "Customer-safe" },
{ id: "escalation-note", label: "Escalation note" },
];

export const INTAKE_FIELDS: Array<{
key: IntakeField;
label: string;
rows?: number;
}> = [
{ key: "issue", label: "Issue summary" },
{ key: "environment", label: "Environment" },
{ key: "impact", label: "Impact", rows: 2 },
{ key: "affected_user", label: "Affected user" },
{ key: "affected_system", label: "Affected system" },
{ key: "affected_site", label: "Affected site" },
{ key: "symptoms", label: "Symptoms", rows: 3 },
{ key: "steps_tried", label: "Steps already tried", rows: 3 },
{ key: "blockers", label: "Current blocker", rows: 2 },
{ key: "likely_category", label: "Likely category" },
];

interface IntakeFieldControlProps {
label: string;
value: string;
rows?: number;
onChange: (value: string) => void;
}

export function IntakeFieldControl({
label,
value,
rows,
onChange,
}: IntakeFieldControlProps) {
if (rows && rows > 1) {
return (
<label className="ticket-workspace-rail__field">
<span>{label}</span>
<textarea
rows={rows}
value={value}
onChange={(event) => onChange(event.target.value)}
/>
</label>
);
}

return (
<label className="ticket-workspace-rail__field">
<span>{label}</span>
<input
type="text"
value={value}
onChange={(event) => onChange(event.target.value)}
/>
</label>
);
}
117 changes: 8 additions & 109 deletions src/features/workspace/TicketWorkspaceRail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,15 @@ import type {
WorkspaceFavorite,
WorkspacePersonalization,
} from "../../types/workspace";
import { markWorkspaceReady } from "./workspacePerformanceMetrics";
import {
INTAKE_FIELDS,
IntakeFieldControl,
NOTE_AUDIENCES,
type IntakeField,
} from "./IntakeFieldControl";
import "./TicketWorkspaceRail.css";

const APP_BOOTSTRAP_START_MARK = "assistsupport:perf:bootstrap-start";
const TICKET_WORKSPACE_READY_MARK = "assistsupport:perf:ticket-workspace-ready";
const TICKET_WORKSPACE_READY_MEASURE =
"assistsupport:perf:ticket-workspace-ready-ms";

type PerfWindow = Window & {
__assistsupportPerf?: {
bootstrapStartedAt: number;
ticketWorkspaceReadyMs?: number;
};
};

type IntakeField =
| "issue"
| "environment"
| "impact"
| "affected_user"
| "affected_system"
| "affected_site"
| "symptoms"
| "steps_tried"
| "blockers"
| "likely_category";

interface TicketWorkspaceRailProps {
intake: CaseIntake;
onIntakeChange: (field: IntakeField, value: string) => void;
Expand Down Expand Up @@ -90,62 +73,6 @@ interface TicketWorkspaceRailProps {
currentResponse: string;
}

const NOTE_AUDIENCES: Array<{ id: NoteAudience; label: string }> = [
{ id: "internal-note", label: "Internal note" },
{ id: "customer-safe", label: "Customer-safe" },
{ id: "escalation-note", label: "Escalation note" },
];

const INTAKE_FIELDS: Array<{ key: IntakeField; label: string; rows?: number }> =
[
{ key: "issue", label: "Issue summary" },
{ key: "environment", label: "Environment" },
{ key: "impact", label: "Impact", rows: 2 },
{ key: "affected_user", label: "Affected user" },
{ key: "affected_system", label: "Affected system" },
{ key: "affected_site", label: "Affected site" },
{ key: "symptoms", label: "Symptoms", rows: 3 },
{ key: "steps_tried", label: "Steps already tried", rows: 3 },
{ key: "blockers", label: "Current blocker", rows: 2 },
{ key: "likely_category", label: "Likely category" },
];

function IntakeFieldControl({
label,
value,
rows,
onChange,
}: {
label: string;
value: string;
rows?: number;
onChange: (value: string) => void;
}) {
if (rows && rows > 1) {
return (
<label className="ticket-workspace-rail__field">
<span>{label}</span>
<textarea
rows={rows}
value={value}
onChange={(event) => onChange(event.target.value)}
/>
</label>
);
}

return (
<label className="ticket-workspace-rail__field">
<span>{label}</span>
<input
type="text"
value={value}
onChange={(event) => onChange(event.target.value)}
/>
</label>
);
}

export function TicketWorkspaceRail({
intake,
onIntakeChange,
Expand Down Expand Up @@ -209,35 +136,7 @@ export function TicketWorkspaceRail({
}, [guidedRunbookSession]);

useEffect(() => {
if (
typeof window === "undefined" ||
typeof window.performance === "undefined"
) {
return;
}

const perfWindow = window as PerfWindow;
const bootstrapStartedAt =
perfWindow.__assistsupportPerf?.bootstrapStartedAt ?? 0;
perfWindow.__assistsupportPerf = {
...(perfWindow.__assistsupportPerf ?? { bootstrapStartedAt }),
bootstrapStartedAt,
ticketWorkspaceReadyMs: Number(
(window.performance.now() - bootstrapStartedAt).toFixed(2),
),
};
window.performance.clearMarks(TICKET_WORKSPACE_READY_MARK);
window.performance.clearMeasures(TICKET_WORKSPACE_READY_MEASURE);
window.performance.mark(TICKET_WORKSPACE_READY_MARK);
try {
window.performance.measure(
TICKET_WORKSPACE_READY_MEASURE,
APP_BOOTSTRAP_START_MARK,
TICKET_WORKSPACE_READY_MARK,
);
} catch {
// Keep the workspace usable even if performance marks are unavailable in the current runtime.
}
markWorkspaceReady();
}, []);

return (
Expand Down
44 changes: 44 additions & 0 deletions src/features/workspace/workspacePerformanceMetrics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
export const APP_BOOTSTRAP_START_MARK = "assistsupport:perf:bootstrap-start";
export const TICKET_WORKSPACE_READY_MARK =
"assistsupport:perf:ticket-workspace-ready";
export const TICKET_WORKSPACE_READY_MEASURE =
"assistsupport:perf:ticket-workspace-ready-ms";

type PerfWindow = Window & {
__assistsupportPerf?: {
bootstrapStartedAt: number;
ticketWorkspaceReadyMs?: number;
};
};

export function markWorkspaceReady(): void {
if (
typeof window === "undefined" ||
typeof window.performance === "undefined"
) {
return;
}

const perfWindow = window as PerfWindow;
const bootstrapStartedAt =
perfWindow.__assistsupportPerf?.bootstrapStartedAt ?? 0;
perfWindow.__assistsupportPerf = {
...(perfWindow.__assistsupportPerf ?? { bootstrapStartedAt }),
bootstrapStartedAt,
ticketWorkspaceReadyMs: Number(
(window.performance.now() - bootstrapStartedAt).toFixed(2),
),
};
window.performance.clearMarks(TICKET_WORKSPACE_READY_MARK);
window.performance.clearMeasures(TICKET_WORKSPACE_READY_MEASURE);
window.performance.mark(TICKET_WORKSPACE_READY_MARK);
try {
window.performance.measure(
TICKET_WORKSPACE_READY_MEASURE,
APP_BOOTSTRAP_START_MARK,
TICKET_WORKSPACE_READY_MARK,
);
} catch {
// Keep the workspace usable even if performance marks are unavailable in the current runtime.
}
}
Loading