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.

- @@ -161,9 +163,9 @@ export function TicketWorkspaceRail({