From 03eb09078fae409211374702c8986349923e854d Mon Sep 17 00:00:00 2001
From: Saagar Patel
Date: Wed, 22 Apr 2026 09:41:44 +0200
Subject: [PATCH] refactor(features): regroup TicketWorkspaceRail props into 8
bundles
Collapse the 38-prop flat interface (deferred from Wave 5.5) into 8
named bundles plus 2 primitives. Each bundle maps to one feature
area: intake, nextActions, similarCases, packs, kits, favorites,
runbooks, personalization. DraftTab shell, rail internals, and the
test suite all updated in lockstep; consumer behavior is unchanged.
---
src/components/Draft/DraftTab.tsx | 88 +++--
.../workspace/TicketWorkspaceRail.test.tsx | 156 +++++----
.../workspace/TicketWorkspaceRail.tsx | 327 +++++++++---------
3 files changed, 308 insertions(+), 263 deletions(-)
diff --git a/src/components/Draft/DraftTab.tsx b/src/components/Draft/DraftTab.tsx
index a6c246d..7d054ae 100644
--- a/src/components/Draft/DraftTab.tsx
+++ b/src/components/Draft/DraftTab.tsx
@@ -1293,42 +1293,58 @@ export const DraftTab = forwardRef(
const workspacePanel = (
setCompareCase(null)}
- handoffPack={handoffPack}
- evidencePack={evidencePack}
- kbDraft={kbDraft}
- onCopyHandoffPack={handleCopyHandoffPack}
- onCopyEvidencePack={handleCopyEvidencePack}
- onCopyKbDraft={handleCopyKbDraft}
- resolutionKits={resolutionKits}
- onSaveResolutionKit={handleSaveCurrentResolutionKit}
- onApplyResolutionKit={handleApplyResolutionKit}
- favorites={workspaceFavorites}
- onToggleFavorite={handleToggleWorkspaceFavorite}
- runbookTemplates={runbookTemplates}
- guidedRunbookSession={guidedRunbookSession}
- runbookNote={guidedRunbookNote}
- onRunbookNoteChange={handleGuidedRunbookNoteChange}
- onStartGuidedRunbook={handleStartGuidedRunbook}
- onAdvanceGuidedRunbook={handleAdvanceGuidedRunbook}
- onCopyRunbookProgressToNotes={handleCopyRunbookProgressToNotes}
- workspacePersonalization={workspacePersonalization}
- onPersonalizationChange={handleWorkspacePersonalizationChange}
+ intake={{
+ data: caseIntake,
+ onChange: handleIntakeFieldChange,
+ onAnalyze: handleAnalyzeIntake,
+ onApplyPreset: handleApplyIntakePreset,
+ onNoteAudienceChange: handleNoteAudienceChange,
+ missingQuestions,
+ }}
+ nextActions={{
+ items: nextActions,
+ onAccept: handleAcceptNextAction,
+ }}
+ similarCases={{
+ items: similarCases,
+ loading: similarCasesLoading,
+ onRefresh: handleRefreshSimilarCases,
+ onOpen: handleOpenSimilarCase,
+ onCompare: handleCompareSimilarCase,
+ onCompareLast: handleCompareLastResolution,
+ compareCase,
+ onCloseCompare: () => setCompareCase(null),
+ }}
+ packs={{
+ handoffPack,
+ evidencePack,
+ kbDraft,
+ onCopyHandoff: handleCopyHandoffPack,
+ onCopyEvidence: handleCopyEvidencePack,
+ onCopyKb: handleCopyKbDraft,
+ }}
+ kits={{
+ items: resolutionKits,
+ onSaveCurrent: handleSaveCurrentResolutionKit,
+ onApply: handleApplyResolutionKit,
+ }}
+ favorites={{
+ items: workspaceFavorites,
+ onToggle: handleToggleWorkspaceFavorite,
+ }}
+ runbooks={{
+ templates: runbookTemplates,
+ session: guidedRunbookSession,
+ note: guidedRunbookNote,
+ onNoteChange: handleGuidedRunbookNoteChange,
+ onStart: handleStartGuidedRunbook,
+ onAdvance: handleAdvanceGuidedRunbook,
+ onCopyProgress: handleCopyRunbookProgressToNotes,
+ }}
+ personalization={{
+ value: workspacePersonalization,
+ onChange: handleWorkspacePersonalizationChange,
+ }}
workspaceCatalogLoading={workspaceCatalogLoading}
currentResponse={response}
/>
diff --git a/src/features/workspace/TicketWorkspaceRail.test.tsx b/src/features/workspace/TicketWorkspaceRail.test.tsx
index fa90bb9..306dcac 100644
--- a/src/features/workspace/TicketWorkspaceRail.test.tsx
+++ b/src/features/workspace/TicketWorkspaceRail.test.tsx
@@ -97,73 +97,91 @@ const basePersonalization: WorkspacePersonalization = {
default_evidence_format: "clipboard",
};
-function renderRail(
- overrides: Partial> = {},
-) {
- const props: ComponentProps = {
- intake: baseIntake,
- onIntakeChange: vi.fn(),
- onAnalyzeIntake: vi.fn(),
- onApplyIntakePreset: vi.fn(),
- onNoteAudienceChange: vi.fn(),
- nextActions: [],
- missingQuestions: [],
- onAcceptNextAction: vi.fn(),
- similarCases: [baseSimilarCase],
- similarCasesLoading: false,
- onRefreshSimilarCases: vi.fn(),
- onOpenSimilarCase: vi.fn(),
- onCompareSimilarCase: vi.fn(),
- onCompareLastResolution: vi.fn(),
- compareCase: null,
- onCloseCompareCase: vi.fn(),
- handoffPack: {
- summary: "VPN issue under review",
- actions_taken: ["Reset VPN profile"],
- current_blocker: "West region still affected",
- next_step: "Escalate to network engineering",
- customer_safe_update: "We are actively working the VPN issue.",
- escalation_note: "Escalate the remaining west region failures.",
+type RailProps = ComponentProps;
+
+function makeBundles(): RailProps {
+ return {
+ intake: {
+ data: baseIntake,
+ onChange: vi.fn(),
+ onAnalyze: vi.fn(),
+ onApplyPreset: vi.fn(),
+ onNoteAudienceChange: vi.fn(),
+ missingQuestions: [],
+ },
+ nextActions: {
+ items: [],
+ onAccept: vi.fn(),
+ },
+ similarCases: {
+ items: [baseSimilarCase],
+ loading: false,
+ onRefresh: vi.fn(),
+ onOpen: vi.fn(),
+ onCompare: vi.fn(),
+ onCompareLast: vi.fn(),
+ compareCase: null,
+ onCloseCompare: vi.fn(),
},
- evidencePack: {
- title: "Evidence Pack · INC-1001",
- summary: "VPN issue under review",
- sections: [],
+ packs: {
+ handoffPack: {
+ summary: "VPN issue under review",
+ actions_taken: ["Reset VPN profile"],
+ current_blocker: "West region still affected",
+ next_step: "Escalate to network engineering",
+ customer_safe_update: "We are actively working the VPN issue.",
+ escalation_note: "Escalate the remaining west region failures.",
+ },
+ evidencePack: {
+ title: "Evidence Pack · INC-1001",
+ summary: "VPN issue under review",
+ sections: [],
+ },
+ kbDraft: {
+ title: "VPN disconnects every morning",
+ summary: "Repeated VPN disconnects for remote users.",
+ symptoms: "Users disconnect every morning.",
+ environment: "Managed Windows laptops",
+ cause: "Likely regional gateway issue",
+ resolution: "Reset profile and escalate to network engineering.",
+ warnings: [],
+ prerequisites: [],
+ policy_links: [],
+ tags: ["incident"],
+ },
+ onCopyHandoff: vi.fn(),
+ onCopyEvidence: vi.fn(),
+ onCopyKb: vi.fn(),
},
- kbDraft: {
- title: "VPN disconnects every morning",
- summary: "Repeated VPN disconnects for remote users.",
- symptoms: "Users disconnect every morning.",
- environment: "Managed Windows laptops",
- cause: "Likely regional gateway issue",
- resolution: "Reset profile and escalate to network engineering.",
- warnings: [],
- prerequisites: [],
- policy_links: [],
- tags: ["incident"],
+ kits: {
+ items: [baseResolutionKit],
+ onSaveCurrent: vi.fn(),
+ onApply: vi.fn(),
+ },
+ favorites: {
+ items: baseFavorites,
+ onToggle: vi.fn(),
+ },
+ runbooks: {
+ templates: [baseRunbookTemplate],
+ session: baseRunbookSession,
+ note: "",
+ onNoteChange: vi.fn(),
+ onStart: vi.fn(),
+ onAdvance: vi.fn(),
+ onCopyProgress: vi.fn(),
+ },
+ personalization: {
+ value: basePersonalization,
+ onChange: vi.fn(),
},
- onCopyHandoffPack: vi.fn(),
- onCopyEvidencePack: vi.fn(),
- onCopyKbDraft: vi.fn(),
- resolutionKits: [baseResolutionKit],
- onSaveResolutionKit: vi.fn(),
- onApplyResolutionKit: vi.fn(),
- favorites: baseFavorites,
- onToggleFavorite: vi.fn(),
- runbookTemplates: [baseRunbookTemplate],
- guidedRunbookSession: baseRunbookSession,
- runbookNote: "",
- onRunbookNoteChange: vi.fn(),
- onStartGuidedRunbook: vi.fn(),
- onAdvanceGuidedRunbook: vi.fn(),
- onCopyRunbookProgressToNotes: vi.fn(),
- workspacePersonalization: basePersonalization,
- onPersonalizationChange: vi.fn(),
workspaceCatalogLoading: false,
currentResponse: "Reset the VPN profile and verify MFA enrollment.",
- ...overrides,
};
+}
+function renderRail(overrides: Partial = {}) {
+ const props: RailProps = { ...makeBundles(), ...overrides };
return {
props,
...render(),
@@ -211,9 +229,9 @@ describe("TicketWorkspaceRail", () => {
}),
);
- expect(props.onCompareLastResolution).toHaveBeenCalledTimes(1);
- expect(props.onApplyResolutionKit).toHaveBeenCalledTimes(1);
- expect(props.onCopyRunbookProgressToNotes).toHaveBeenCalledTimes(1);
+ expect(props.similarCases.onCompareLast).toHaveBeenCalledTimes(1);
+ expect(props.kits.onApply).toHaveBeenCalledTimes(1);
+ expect(props.runbooks.onCopyProgress).toHaveBeenCalledTimes(1);
expect(screen.getByText("Favorites")).toBeTruthy();
expect(screen.getByText("Guided runbooks")).toBeTruthy();
});
@@ -243,19 +261,19 @@ describe("TicketWorkspaceRail", () => {
{ target: { value: "Long" } },
);
- expect(props.onPersonalizationChange).toHaveBeenCalledWith({
+ expect(props.personalization.onChange).toHaveBeenCalledWith({
preferred_output_length: "Long",
});
});
it("shows empty states when the catalog is unavailable and compare is not ready", () => {
+ const base = makeBundles();
const { container } = renderRail({
currentResponse: "",
- similarCases: [],
- resolutionKits: [],
- favorites: [],
- guidedRunbookSession: null,
- runbookTemplates: [],
+ similarCases: { ...base.similarCases, items: [] },
+ kits: { ...base.kits, items: [] },
+ favorites: { ...base.favorites, items: [] },
+ runbooks: { ...base.runbooks, session: null, templates: [] },
});
const rail = getRailRoot(container);
diff --git a/src/features/workspace/TicketWorkspaceRail.tsx b/src/features/workspace/TicketWorkspaceRail.tsx
index d135297..6cdb539 100644
--- a/src/features/workspace/TicketWorkspaceRail.tsx
+++ b/src/features/workspace/TicketWorkspaceRail.tsx
@@ -25,115 +25,117 @@ import {
} from "./IntakeFieldControl";
import "./TicketWorkspaceRail.css";
-interface TicketWorkspaceRailProps {
- intake: CaseIntake;
- onIntakeChange: (field: IntakeField, value: string) => void;
- onAnalyzeIntake: () => void;
- onApplyIntakePreset: (
- preset: "incident" | "access" | "rollout" | "device",
- ) => void;
+export interface TicketWorkspaceRailIntakeBundle {
+ data: CaseIntake;
+ onChange: (field: IntakeField, value: string) => void;
+ onAnalyze: () => void;
+ onApplyPreset: (preset: "incident" | "access" | "rollout" | "device") => void;
onNoteAudienceChange: (audience: NoteAudience) => void;
- nextActions: NextActionRecommendation[];
missingQuestions: MissingQuestion[];
- onAcceptNextAction: (action: NextActionRecommendation) => void;
- similarCases: SimilarCase[];
- similarCasesLoading: boolean;
- onRefreshSimilarCases: () => void;
- onOpenSimilarCase: (similarCase: SimilarCase) => void;
- onCompareSimilarCase: (similarCase: SimilarCase) => void;
- onCompareLastResolution: () => void;
+}
+
+export interface TicketWorkspaceRailNextActionsBundle {
+ items: NextActionRecommendation[];
+ onAccept: (action: NextActionRecommendation) => void;
+}
+
+export interface TicketWorkspaceRailSimilarCasesBundle {
+ items: SimilarCase[];
+ loading: boolean;
+ onRefresh: () => void;
+ onOpen: (similarCase: SimilarCase) => void;
+ onCompare: (similarCase: SimilarCase) => void;
+ onCompareLast: () => void;
compareCase: SimilarCase | null;
- onCloseCompareCase: () => void;
+ onCloseCompare: () => void;
+}
+
+export interface TicketWorkspaceRailPacksBundle {
handoffPack: HandoffPack;
evidencePack: EvidencePack;
kbDraft: KbDraft;
- onCopyHandoffPack: () => void;
- onCopyEvidencePack: () => void;
- onCopyKbDraft: () => void;
- resolutionKits: ResolutionKit[];
- onSaveResolutionKit: () => void;
- onApplyResolutionKit: (kit: ResolutionKit) => void;
- favorites: WorkspaceFavorite[];
- onToggleFavorite: (
+ onCopyHandoff: () => void;
+ onCopyEvidence: () => void;
+ onCopyKb: () => void;
+}
+
+export interface TicketWorkspaceRailKitsBundle {
+ items: ResolutionKit[];
+ onSaveCurrent: () => void;
+ onApply: (kit: ResolutionKit) => void;
+}
+
+export interface TicketWorkspaceRailFavoritesBundle {
+ items: WorkspaceFavorite[];
+ onToggle: (
kind: WorkspaceFavorite["kind"],
resourceId: string,
label: string,
metadata?: Record | null,
) => void;
- runbookTemplates: GuidedRunbookTemplate[];
- guidedRunbookSession: GuidedRunbookSession | null;
- runbookNote: string;
- onRunbookNoteChange: (value: string) => void;
- onStartGuidedRunbook: (templateId: string) => void;
- onAdvanceGuidedRunbook: (status: "completed" | "skipped" | "failed") => void;
- onCopyRunbookProgressToNotes: () => void;
- workspacePersonalization: WorkspacePersonalization;
- onPersonalizationChange: (patch: Partial) => void;
+}
+
+export interface TicketWorkspaceRailRunbooksBundle {
+ templates: GuidedRunbookTemplate[];
+ session: GuidedRunbookSession | null;
+ note: string;
+ onNoteChange: (value: string) => void;
+ onStart: (templateId: string) => void;
+ onAdvance: (status: "completed" | "skipped" | "failed") => void;
+ onCopyProgress: () => void;
+}
+
+export interface TicketWorkspaceRailPersonalizationBundle {
+ value: WorkspacePersonalization;
+ onChange: (patch: Partial) => void;
+}
+
+interface TicketWorkspaceRailProps {
+ intake: TicketWorkspaceRailIntakeBundle;
+ nextActions: TicketWorkspaceRailNextActionsBundle;
+ similarCases: TicketWorkspaceRailSimilarCasesBundle;
+ packs: TicketWorkspaceRailPacksBundle;
+ kits: TicketWorkspaceRailKitsBundle;
+ favorites: TicketWorkspaceRailFavoritesBundle;
+ runbooks: TicketWorkspaceRailRunbooksBundle;
+ personalization: TicketWorkspaceRailPersonalizationBundle;
workspaceCatalogLoading: boolean;
currentResponse: string;
}
export function TicketWorkspaceRail({
intake,
- onIntakeChange,
- onAnalyzeIntake,
- onApplyIntakePreset,
- onNoteAudienceChange,
nextActions,
- missingQuestions,
- onAcceptNextAction,
similarCases,
- similarCasesLoading,
- onRefreshSimilarCases,
- onOpenSimilarCase,
- onCompareSimilarCase,
- onCompareLastResolution,
- compareCase,
- onCloseCompareCase,
- handoffPack,
- evidencePack,
- kbDraft,
- onCopyHandoffPack,
- onCopyEvidencePack,
- onCopyKbDraft,
- resolutionKits,
- onSaveResolutionKit,
- onApplyResolutionKit,
+ packs,
+ kits,
favorites,
- onToggleFavorite,
- runbookTemplates,
- guidedRunbookSession,
- runbookNote,
- onRunbookNoteChange,
- onStartGuidedRunbook,
- onAdvanceGuidedRunbook,
- onCopyRunbookProgressToNotes,
- workspacePersonalization,
- onPersonalizationChange,
+ runbooks,
+ personalization,
workspaceCatalogLoading,
currentResponse,
}: TicketWorkspaceRailProps) {
const [selectedRunbookTemplateId, setSelectedRunbookTemplateId] =
useState("");
const intakeMissing = useMemo(
- () => intake.missing_data ?? [],
- [intake.missing_data],
+ () => intake.data.missing_data ?? [],
+ [intake.data.missing_data],
);
const favoriteLookup = useMemo(
() =>
new Set(
- favorites.map((favorite) => `${favorite.kind}:${favorite.resource_id}`),
+ favorites.items.map(
+ (favorite) => `${favorite.kind}:${favorite.resource_id}`,
+ ),
),
- [favorites],
+ [favorites.items],
);
const currentRunbookStepLabel = useMemo(() => {
- if (!guidedRunbookSession) {
+ if (!runbooks.session) {
return null;
}
- return (
- guidedRunbookSession.steps[guidedRunbookSession.current_step] ?? null
- );
- }, [guidedRunbookSession]);
+ return runbooks.session.steps[runbooks.session.current_step] ?? null;
+ }, [runbooks.session]);
useEffect(() => {
markWorkspaceReady();
@@ -150,7 +152,7 @@ export function TicketWorkspaceRail({
draft.
-