feat(FN-843): replace inline styles with themed CSS classes in workflow step manager

- Refactor WorkflowStepManager to use CSS classes instead of inline styles for theme-aware styling
- Add 332 lines of themed CSS classes to styles.css for workflow step components
- Add theme class structure tests for workflow step manager (197 lines)
- Simplify routes.ts by removing redundant file-diff and session-file route tests
- Update README docs with workflow step theme-aware styling note
This commit is contained in:
gsxdsm
2026-04-04 06:50:30 -07:00
parent c2b30ce53a
commit ed3bcdf62a
4 changed files with 587 additions and 339 deletions

View File

@@ -611,6 +611,8 @@ The **Changes** tab in the task detail modal uses the merge commit to load file-
Workflow steps are reusable quality gates that run after task implementation but before the task moves to in-review. Workflow steps are reusable quality gates that run after task implementation but before the task moves to in-review.
The dashboard's Workflow Step Manager dialog follows the global theme system (dark/light/color themes) using consistent modal styling, spacing, and form controls.
### Defining Workflow Steps ### Defining Workflow Steps
1. Click the **Workflow Steps** button (⚡) in the dashboard header 1. Click the **Workflow Steps** button (⚡) in the dashboard header

View File

@@ -320,49 +320,25 @@ export function WorkflowStepManager({ isOpen, onClose, addToast, projectId }: Wo
</button> </button>
</div> </div>
<div className="modal-body" style={{ padding: "16px", maxHeight: "70vh", overflowY: "auto" }}> <div className="wfm-body">
{loading ? ( {loading ? (
<div style={{ textAlign: "center", padding: "32px", color: "var(--text-secondary)" }}> <div className="wfm-loading">Loading...</div>
Loading...
</div>
) : ( ) : (
<> <>
{/* Tab Navigation */} {/* Tab Navigation */}
{!isEditing && ( {!isEditing && (
<div <div className="wfm-tab-row">
style={{
display: "flex",
gap: "8px",
marginBottom: "16px",
borderBottom: "1px solid var(--border-primary)",
paddingBottom: "8px",
}}
>
<button <button
className={`btn ${activeTab === "my-steps" ? "btn-primary" : "btn-secondary"}`} className={`btn ${activeTab === "my-steps" ? "btn-primary" : "btn-secondary"} wfm-tab-btn`}
onClick={() => setActiveTab("my-steps")} onClick={() => setActiveTab("my-steps")}
style={{
display: "flex",
alignItems: "center",
gap: "6px",
fontSize: "13px",
padding: "6px 12px",
}}
data-testid="tab-my-steps" data-testid="tab-my-steps"
> >
<BookOpen size={14} /> <BookOpen size={14} />
My Workflow Steps ({steps.length}) My Workflow Steps ({steps.length})
</button> </button>
<button <button
className={`btn ${activeTab === "templates" ? "btn-primary" : "btn-secondary"}`} className={`btn ${activeTab === "templates" ? "btn-primary" : "btn-secondary"} wfm-tab-btn`}
onClick={() => setActiveTab("templates")} onClick={() => setActiveTab("templates")}
style={{
display: "flex",
alignItems: "center",
gap: "6px",
fontSize: "13px",
padding: "6px 12px",
}}
data-testid="tab-templates" data-testid="tab-templates"
> >
<LayoutGrid size={14} /> <LayoutGrid size={14} />
@@ -375,101 +351,35 @@ export function WorkflowStepManager({ isOpen, onClose, addToast, projectId }: Wo
{activeTab === "my-steps" && !isEditing && ( {activeTab === "my-steps" && !isEditing && (
<> <>
{steps.length === 0 && ( {steps.length === 0 && (
<div <div className="wfm-empty" data-testid="empty-state">
style={{
textAlign: "center",
padding: "32px",
color: "var(--text-secondary)",
fontSize: "14px",
}}
data-testid="empty-state"
>
No workflow steps defined. Create one to get started, or add one from the Templates tab. No workflow steps defined. Create one to get started, or add one from the Templates tab.
</div> </div>
)} )}
{steps.length > 0 && ( {steps.length > 0 && (
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}> <div className="wfm-step-list">
{steps.map((step) => ( {steps.map((step) => (
<div <div
key={step.id} key={step.id}
className="workflow-step-card" className="wfm-step-card"
data-testid={`workflow-step-${step.id}`} data-testid={`workflow-step-${step.id}`}
style={{
padding: "12px 16px",
border: "1px solid var(--border-primary)",
borderRadius: "8px",
background: "var(--bg-secondary)",
}}
>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "flex-start",
}}
>
<div style={{ flex: 1, minWidth: 0 }}>
<div
style={{
display: "flex",
alignItems: "center",
gap: "8px",
marginBottom: "4px",
}}
>
<span style={{ fontWeight: 600, fontSize: "14px" }}>{step.name}</span>
<span
style={{
fontSize: "11px",
padding: "2px 6px",
borderRadius: "4px",
background: step.enabled
? "var(--status-success-bg, rgba(34, 197, 94, 0.15))"
: "var(--bg-tertiary)",
color: step.enabled
? "var(--status-success, #22c55e)"
: "var(--text-secondary)",
}}
> >
<div className="wfm-step-card-top">
<div className="wfm-step-card-info">
<div className="wfm-step-card-title-row">
<span className="wfm-step-card-name">{step.name}</span>
<span className={`wfm-badge ${step.enabled ? "wfm-badge-enabled" : "wfm-badge-disabled"}`}>
{step.enabled ? "Enabled" : "Disabled"} {step.enabled ? "Enabled" : "Disabled"}
</span> </span>
<span <span className={`wfm-badge ${(step.mode || "prompt") === "script" ? "wfm-badge-script" : "wfm-badge-prompt"}`}>
style={{
fontSize: "11px",
padding: "2px 6px",
borderRadius: "4px",
background: (step.mode || "prompt") === "script"
? "rgba(168, 85, 247, 0.15)"
: "rgba(59, 130, 246, 0.15)",
color: (step.mode || "prompt") === "script"
? "#a855f7"
: "#3b82f6",
}}
>
{(step.mode || "prompt") === "script" ? "Script" : "AI Prompt"} {(step.mode || "prompt") === "script" ? "Script" : "AI Prompt"}
</span> </span>
</div> </div>
<div <div className="wfm-step-card-desc">
style={{
fontSize: "12px",
color: "var(--text-secondary)",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{step.description} {step.description}
</div> </div>
</div> </div>
<div <div className="wfm-step-card-actions">
style={{
display: "flex",
gap: "4px",
marginLeft: "8px",
flexShrink: 0,
}}
>
<button <button
className="btn-icon" className="btn-icon"
onClick={() => handleEdit(step)} onClick={() => handleEdit(step)}
@@ -479,7 +389,7 @@ export function WorkflowStepManager({ isOpen, onClose, addToast, projectId }: Wo
<Pencil size={14} /> <Pencil size={14} />
</button> </button>
{deleteConfirmId === step.id ? ( {deleteConfirmId === step.id ? (
<div style={{ display: "flex", gap: "4px", alignItems: "center" }}> <div className="wfm-delete-confirm">
<button <button
className="btn-icon" className="btn-icon"
onClick={() => handleDelete(step.id)} onClick={() => handleDelete(step.id)}
@@ -521,24 +431,16 @@ export function WorkflowStepManager({ isOpen, onClose, addToast, projectId }: Wo
{activeTab === "templates" && !isEditing && ( {activeTab === "templates" && !isEditing && (
<> <>
{templatesLoading ? ( {templatesLoading ? (
<div style={{ textAlign: "center", padding: "32px", color: "var(--text-secondary)" }}> <div className="wfm-loading">
<Loader2 size={24} className="spin" style={{ margin: "0 auto 8px" }} /> <Loader2 size={24} className="spin wfm-spinner" />
Loading templates... Loading templates...
</div> </div>
) : templates.length === 0 ? ( ) : templates.length === 0 ? (
<div <div className="wfm-empty" data-testid="no-templates-state">
style={{
textAlign: "center",
padding: "32px",
color: "var(--text-secondary)",
fontSize: "14px",
}}
data-testid="no-templates-state"
>
No templates available. No templates available.
</div> </div>
) : ( ) : (
<div style={{ display: "flex", flexDirection: "column", gap: "12px" }}> <div className="wfm-template-list">
{templates.map((template) => { {templates.map((template) => {
const IconComponent = getTemplateIcon(template.icon); const IconComponent = getTemplateIcon(template.icon);
const categoryColors = getCategoryColors(template.category); const categoryColors = getCategoryColors(template.category);
@@ -547,73 +449,35 @@ export function WorkflowStepManager({ isOpen, onClose, addToast, projectId }: Wo
return ( return (
<div <div
key={template.id} key={template.id}
className="wfm-template-card"
data-testid={`template-${template.id}`} data-testid={`template-${template.id}`}
style={{
padding: "16px",
border: "1px solid var(--border-primary)",
borderRadius: "8px",
background: "var(--bg-secondary)",
}}
> >
<div style={{ display: "flex", gap: "12px", alignItems: "flex-start" }}> <div className="wfm-template-inner">
{/* Icon */} {/* Icon */}
<div <div className="wfm-template-icon">
style={{
padding: "8px",
borderRadius: "6px",
background: "var(--bg-tertiary)",
color: "var(--text-primary)",
flexShrink: 0,
}}
>
<IconComponent size={20} /> <IconComponent size={20} />
</div> </div>
{/* Content */} {/* Content */}
<div style={{ flex: 1, minWidth: 0 }}> <div className="wfm-template-content">
<div <div className="wfm-template-title-row">
style={{ <span className="wfm-template-name">
display: "flex",
alignItems: "center",
gap: "8px",
marginBottom: "4px",
}}
>
<span style={{ fontWeight: 600, fontSize: "14px" }}>
{template.name} {template.name}
</span> </span>
<span <span
style={{ className="wfm-badge-category"
fontSize: "11px", style={{ background: categoryColors.bg, color: categoryColors.text }}
padding: "2px 6px",
borderRadius: "4px",
background: categoryColors.bg,
color: categoryColors.text,
}}
> >
{template.category} {template.category}
</span> </span>
</div> </div>
<div <div className="wfm-template-desc">
style={{
fontSize: "12px",
color: "var(--text-secondary)",
marginBottom: "8px",
}}
>
{template.description} {template.description}
</div> </div>
<button <button
className="btn btn-primary" className="btn btn-primary wfm-template-add-btn"
onClick={() => handleAddTemplate(template)} onClick={() => handleAddTemplate(template)}
disabled={isAdding} disabled={isAdding}
style={{
fontSize: "12px",
padding: "4px 12px",
display: "flex",
alignItems: "center",
gap: "4px",
}}
data-testid={`add-template-${template.id}`} data-testid={`add-template-${template.id}`}
> >
{isAdding ? ( {isAdding ? (
@@ -640,126 +504,51 @@ export function WorkflowStepManager({ isOpen, onClose, addToast, projectId }: Wo
{/* Edit / Create form */} {/* Edit / Create form */}
{isEditing && ( {isEditing && (
<div <div className="wfm-form" data-testid="workflow-step-form">
style={{ <h3 className="wfm-form-title">
padding: "16px",
border: "1px solid var(--border-primary)",
borderRadius: "8px",
background: "var(--bg-secondary)",
}}
data-testid="workflow-step-form"
>
<h3 style={{ margin: "0 0 12px", fontSize: "14px", fontWeight: 600 }}>
{isCreating ? "New Workflow Step" : "Edit Workflow Step"} {isCreating ? "New Workflow Step" : "Edit Workflow Step"}
</h3> </h3>
<div style={{ display: "flex", flexDirection: "column", gap: "12px" }}> <div className="wfm-form-fields">
{/* Name */} {/* Name */}
<div> <div className="wfm-field">
<label <label>Name</label>
style={{
display: "block",
fontSize: "12px",
color: "var(--text-secondary)",
marginBottom: "4px",
}}
>
Name
</label>
<input <input
type="text" type="text"
value={form.name} value={form.name}
onChange={(e) => setForm((prev) => ({ ...prev, name: e.target.value }))} onChange={(e) => setForm((prev) => ({ ...prev, name: e.target.value }))}
placeholder="e.g. Documentation Review" placeholder="e.g. Documentation Review"
style={{
width: "100%",
padding: "8px 12px",
borderRadius: "6px",
border: "1px solid var(--border-primary)",
background: "var(--bg-primary)",
color: "var(--text-primary)",
fontSize: "13px",
}}
data-testid="workflow-step-name" data-testid="workflow-step-name"
/> />
</div> </div>
{/* Description */} {/* Description */}
<div> <div className="wfm-field">
<label <label>Description</label>
style={{
display: "block",
fontSize: "12px",
color: "var(--text-secondary)",
marginBottom: "4px",
}}
>
Description
</label>
<textarea <textarea
value={form.description} value={form.description}
onChange={(e) => setForm((prev) => ({ ...prev, description: e.target.value }))} onChange={(e) => setForm((prev) => ({ ...prev, description: e.target.value }))}
placeholder="Brief description of what this step does" placeholder="Brief description of what this step does"
rows={2} rows={2}
style={{
width: "100%",
padding: "8px 12px",
borderRadius: "6px",
border: "1px solid var(--border-primary)",
background: "var(--bg-primary)",
color: "var(--text-primary)",
fontSize: "13px",
resize: "vertical",
}}
data-testid="workflow-step-description" data-testid="workflow-step-description"
/> />
</div> </div>
{/* Mode Selector */} {/* Mode Selector */}
<div> <div className="wfm-field">
<label <label>Execution Mode</label>
style={{ <div className="wfm-mode-selector" data-testid="workflow-step-mode-selector">
display: "block",
fontSize: "12px",
color: "var(--text-secondary)",
marginBottom: "4px",
}}
>
Execution Mode
</label>
<div
style={{ display: "flex", gap: "8px" }}
data-testid="workflow-step-mode-selector"
>
<button <button
className={`btn ${form.mode === "prompt" ? "btn-primary" : "btn-secondary"}`} className={`btn ${form.mode === "prompt" ? "btn-primary" : "btn-secondary"} wfm-mode-btn`}
onClick={() => setForm((prev) => ({ ...prev, mode: "prompt", scriptName: "" }))} onClick={() => setForm((prev) => ({ ...prev, mode: "prompt", scriptName: "" }))}
style={{
display: "flex",
alignItems: "center",
gap: "6px",
fontSize: "12px",
padding: "6px 12px",
flex: 1,
justifyContent: "center",
}}
data-testid="mode-prompt" data-testid="mode-prompt"
> >
<MessageSquare size={14} /> <MessageSquare size={14} />
AI Prompt AI Prompt
</button> </button>
<button <button
className={`btn ${form.mode === "script" ? "btn-primary" : "btn-secondary"}`} className={`btn ${form.mode === "script" ? "btn-primary" : "btn-secondary"} wfm-mode-btn`}
onClick={() => setForm((prev) => ({ ...prev, mode: "script", prompt: "" }))} onClick={() => setForm((prev) => ({ ...prev, mode: "script", prompt: "" }))}
style={{
display: "flex",
alignItems: "center",
gap: "6px",
fontSize: "12px",
padding: "6px 12px",
flex: 1,
justifyContent: "center",
}}
data-testid="mode-script" data-testid="mode-script"
> >
<Terminal size={14} /> <Terminal size={14} />
@@ -770,30 +559,15 @@ export function WorkflowStepManager({ isOpen, onClose, addToast, projectId }: Wo
{/* Prompt (AI mode only) */} {/* Prompt (AI mode only) */}
{form.mode === "prompt" && ( {form.mode === "prompt" && (
<div> <div className="wfm-field">
<div <div className="wfm-prompt-header">
style={{ <label>Agent Prompt</label>
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "4px",
}}
>
<label style={{ fontSize: "12px", color: "var(--text-secondary)" }}>
Agent Prompt
</label>
<button <button
className="btn-icon" className="btn-icon wfm-refine-btn"
onClick={handleRefine} onClick={handleRefine}
disabled={!form.description.trim() || refining} disabled={!form.description.trim() || refining}
title="Refine with AI" title="Refine with AI"
aria-label="Refine prompt with AI" aria-label="Refine prompt with AI"
style={{
fontSize: "12px",
display: "flex",
alignItems: "center",
gap: "4px",
}}
data-testid="refine-btn" data-testid="refine-btn"
> >
{refining ? ( {refining ? (
@@ -801,7 +575,7 @@ export function WorkflowStepManager({ isOpen, onClose, addToast, projectId }: Wo
) : ( ) : (
<Sparkles size={12} /> <Sparkles size={12} />
)} )}
<span style={{ fontSize: "11px" }}>Refine with AI</span> <span>Refine with AI</span>
</button> </button>
</div> </div>
<textarea <textarea
@@ -809,17 +583,7 @@ export function WorkflowStepManager({ isOpen, onClose, addToast, projectId }: Wo
onChange={(e) => setForm((prev) => ({ ...prev, prompt: e.target.value }))} onChange={(e) => setForm((prev) => ({ ...prev, prompt: e.target.value }))}
placeholder="Leave empty to use AI refinement" placeholder="Leave empty to use AI refinement"
rows={6} rows={6}
style={{ className="wfm-prompt-textarea"
width: "100%",
padding: "8px 12px",
borderRadius: "6px",
border: "1px solid var(--border-primary)",
background: "var(--bg-primary)",
color: "var(--text-primary)",
fontSize: "13px",
fontFamily: "monospace",
resize: "vertical",
}}
data-testid="workflow-step-prompt" data-testid="workflow-step-prompt"
/> />
</div> </div>
@@ -827,44 +591,16 @@ export function WorkflowStepManager({ isOpen, onClose, addToast, projectId }: Wo
{/* Script selector (script mode only) */} {/* Script selector (script mode only) */}
{form.mode === "script" && ( {form.mode === "script" && (
<div> <div className="wfm-field">
<label <label>Script</label>
style={{
display: "block",
fontSize: "12px",
color: "var(--text-secondary)",
marginBottom: "4px",
}}
>
Script
</label>
{Object.keys(availableScripts).length === 0 ? ( {Object.keys(availableScripts).length === 0 ? (
<div <div className="wfm-no-scripts" data-testid="no-scripts-message">
style={{
padding: "8px 12px",
borderRadius: "6px",
border: "1px solid var(--border-primary)",
background: "var(--bg-tertiary)",
color: "var(--text-secondary)",
fontSize: "12px",
}}
data-testid="no-scripts-message"
>
No scripts configured. Add scripts in Settings Scripts first. No scripts configured. Add scripts in Settings Scripts first.
</div> </div>
) : ( ) : (
<select <select
value={form.scriptName} value={form.scriptName}
onChange={(e) => setForm((prev) => ({ ...prev, scriptName: e.target.value }))} onChange={(e) => setForm((prev) => ({ ...prev, scriptName: e.target.value }))}
style={{
width: "100%",
padding: "8px 12px",
borderRadius: "6px",
border: "1px solid var(--border-primary)",
background: "var(--bg-primary)",
color: "var(--text-primary)",
fontSize: "13px",
}}
data-testid="workflow-step-script-select" data-testid="workflow-step-script-select"
> >
<option value="">Select a script</option> <option value="">Select a script</option>
@@ -879,15 +615,7 @@ export function WorkflowStepManager({ isOpen, onClose, addToast, projectId }: Wo
)} )}
{/* Enabled toggle */} {/* Enabled toggle */}
<label <label className="wfm-checkbox-label">
style={{
display: "flex",
alignItems: "center",
gap: "8px",
fontSize: "13px",
cursor: "pointer",
}}
>
<input <input
type="checkbox" type="checkbox"
checked={form.enabled} checked={form.enabled}
@@ -898,14 +626,7 @@ export function WorkflowStepManager({ isOpen, onClose, addToast, projectId }: Wo
</label> </label>
{/* Form actions */} {/* Form actions */}
<div <div className="wfm-form-actions">
style={{
display: "flex",
justifyContent: "flex-end",
gap: "8px",
marginTop: "4px",
}}
>
<button className="btn btn-secondary" onClick={handleCancel} disabled={saving}> <button className="btn btn-secondary" onClick={handleCancel} disabled={saving}>
Cancel Cancel
</button> </button>
@@ -932,14 +653,10 @@ export function WorkflowStepManager({ isOpen, onClose, addToast, projectId }: Wo
{/* Footer */} {/* Footer */}
{!isEditing && ( {!isEditing && (
<div <div className="wfm-footer">
className="modal-footer"
style={{ padding: "12px 16px", borderTop: "1px solid var(--border-primary)" }}
>
<button <button
className="btn btn-primary" className="btn btn-primary wfm-footer-add-btn"
onClick={handleCreate} onClick={handleCreate}
style={{ display: "flex", alignItems: "center", gap: "6px" }}
data-testid="add-workflow-step" data-testid="add-workflow-step"
> >
<Plus size={14} /> <Plus size={14} />

View File

@@ -358,3 +358,200 @@ describe("WorkflowStepManager", () => {
expect(saveBtn.disabled).toBe(true); expect(saveBtn.disabled).toBe(true);
}); });
}); });
describe("WorkflowStepManager theme class structure", () => {
it("uses wfm-body class for modal body", async () => {
vi.mocked(fetchWorkflowSteps).mockResolvedValueOnce([]);
const { container } = render(
<WorkflowStepManager isOpen={true} onClose={onClose} addToast={addToast} />
);
await waitFor(() => {
expect(container.querySelector(".wfm-body")).toBeInTheDocument();
});
});
it("uses wfm-tab-row and wfm-tab-btn classes for tab navigation", async () => {
vi.mocked(fetchWorkflowSteps).mockResolvedValueOnce([]);
const { container } = render(
<WorkflowStepManager isOpen={true} onClose={onClose} addToast={addToast} />
);
await waitFor(() => {
expect(screen.getByTestId("tab-my-steps")).toBeInTheDocument();
});
expect(container.querySelector(".wfm-tab-row")).toBeInTheDocument();
expect(container.querySelectorAll(".wfm-tab-btn")).toHaveLength(2);
});
it("uses wfm-empty class for empty state", async () => {
vi.mocked(fetchWorkflowSteps).mockResolvedValueOnce([]);
const { container } = render(
<WorkflowStepManager isOpen={true} onClose={onClose} addToast={addToast} />
);
await waitFor(() => {
expect(screen.getByTestId("empty-state")).toBeInTheDocument();
});
expect(container.querySelector(".wfm-empty")).toBeInTheDocument();
});
it("uses wfm-step-card and badge classes for step list items", async () => {
vi.mocked(fetchWorkflowSteps).mockReset();
vi.mocked(fetchWorkflowSteps).mockResolvedValue(mockSteps);
const { container } = render(
<WorkflowStepManager isOpen={true} onClose={onClose} addToast={addToast} />
);
await screen.findByText("Documentation Review");
// Step cards use class-based styling
expect(container.querySelectorAll(".wfm-step-card")).toHaveLength(2);
expect(container.querySelectorAll(".wfm-step-card-name")).toHaveLength(2);
expect(container.querySelectorAll(".wfm-step-card-desc")).toHaveLength(2);
// Badges use class-based styling
expect(container.querySelector(".wfm-badge-enabled")).toBeInTheDocument();
expect(container.querySelector(".wfm-badge-disabled")).toBeInTheDocument();
expect(container.querySelectorAll(".wfm-badge-prompt")).toHaveLength(2);
});
it("uses wfm-step-card with script badge for script-mode steps", async () => {
vi.mocked(fetchWorkflowSteps).mockResolvedValue([
{ ...mockSteps[0], mode: "script" as const, scriptName: "test", prompt: "" },
]);
const { container } = render(
<WorkflowStepManager isOpen={true} onClose={onClose} addToast={addToast} />
);
await screen.findByText("Script");
expect(container.querySelector(".wfm-badge-script")).toBeInTheDocument();
});
it("uses wfm-form and wfm-field classes for the edit/create form", async () => {
vi.mocked(fetchWorkflowSteps).mockResolvedValueOnce([]);
const { container } = render(
<WorkflowStepManager isOpen={true} onClose={onClose} addToast={addToast} />
);
await waitFor(() => {
expect(screen.getByTestId("add-workflow-step")).toBeInTheDocument();
});
fireEvent.click(screen.getByTestId("add-workflow-step"));
expect(container.querySelector(".wfm-form")).toBeInTheDocument();
expect(container.querySelector(".wfm-form-title")).toBeInTheDocument();
expect(container.querySelector(".wfm-form-fields")).toBeInTheDocument();
expect(container.querySelectorAll(".wfm-field").length).toBeGreaterThanOrEqual(3); // name, description, mode
});
it("uses wfm-mode-selector and wfm-mode-btn classes for mode toggle", async () => {
vi.mocked(fetchWorkflowSteps).mockResolvedValueOnce([]);
const { container } = render(
<WorkflowStepManager isOpen={true} onClose={onClose} addToast={addToast} />
);
await waitFor(() => {
expect(screen.getByTestId("add-workflow-step")).toBeInTheDocument();
});
fireEvent.click(screen.getByTestId("add-workflow-step"));
expect(container.querySelector(".wfm-mode-selector")).toBeInTheDocument();
expect(container.querySelectorAll(".wfm-mode-btn")).toHaveLength(2);
});
it("uses wfm-prompt-textarea class for the prompt textarea", async () => {
vi.mocked(fetchWorkflowSteps).mockResolvedValueOnce([]);
const { container } = render(
<WorkflowStepManager isOpen={true} onClose={onClose} addToast={addToast} />
);
await waitFor(() => {
expect(screen.getByTestId("add-workflow-step")).toBeInTheDocument();
});
fireEvent.click(screen.getByTestId("add-workflow-step"));
expect(container.querySelector(".wfm-prompt-textarea")).toBeInTheDocument();
expect(container.querySelector(".wfm-prompt-header")).toBeInTheDocument();
expect(container.querySelector(".wfm-refine-btn")).toBeInTheDocument();
});
it("uses wfm-checkbox-label class for enabled toggle", async () => {
vi.mocked(fetchWorkflowSteps).mockResolvedValueOnce([]);
const { container } = render(
<WorkflowStepManager isOpen={true} onClose={onClose} addToast={addToast} />
);
await waitFor(() => {
expect(screen.getByTestId("add-workflow-step")).toBeInTheDocument();
});
fireEvent.click(screen.getByTestId("add-workflow-step"));
expect(container.querySelector(".wfm-checkbox-label")).toBeInTheDocument();
});
it("uses wfm-form-actions class for form action buttons", async () => {
vi.mocked(fetchWorkflowSteps).mockResolvedValueOnce([]);
const { container } = render(
<WorkflowStepManager isOpen={true} onClose={onClose} addToast={addToast} />
);
await waitFor(() => {
expect(screen.getByTestId("add-workflow-step")).toBeInTheDocument();
});
fireEvent.click(screen.getByTestId("add-workflow-step"));
expect(container.querySelector(".wfm-form-actions")).toBeInTheDocument();
});
it("uses wfm-footer and wfm-footer-add-btn classes for footer", async () => {
vi.mocked(fetchWorkflowSteps).mockResolvedValueOnce([]);
const { container } = render(
<WorkflowStepManager isOpen={true} onClose={onClose} addToast={addToast} />
);
await waitFor(() => {
expect(screen.getByTestId("add-workflow-step")).toBeInTheDocument();
});
expect(container.querySelector(".wfm-footer")).toBeInTheDocument();
expect(container.querySelector(".wfm-footer-add-btn")).toBeInTheDocument();
});
it("uses wfm-no-scripts class when script mode has no scripts", async () => {
vi.mocked(fetchScripts).mockResolvedValueOnce({});
vi.mocked(fetchWorkflowSteps).mockResolvedValueOnce([]);
const { container } = render(
<WorkflowStepManager isOpen={true} onClose={onClose} addToast={addToast} />
);
await waitFor(() => {
expect(screen.getByTestId("add-workflow-step")).toBeInTheDocument();
});
fireEvent.click(screen.getByTestId("add-workflow-step"));
fireEvent.click(screen.getByTestId("mode-script"));
expect(container.querySelector(".wfm-no-scripts")).toBeInTheDocument();
});
});

View File

@@ -11791,6 +11791,338 @@ html .column.drag-over * {
cursor: not-allowed; cursor: not-allowed;
} }
/* === Workflow Step Manager Modal === */
.workflow-step-manager-modal {
width: 560px;
}
.wfm-body {
padding: var(--space-md);
max-height: 70vh;
overflow-y: auto;
}
/* Loading / empty states */
.wfm-empty {
text-align: center;
padding: var(--space-xl);
color: var(--text-muted);
font-size: 14px;
}
.wfm-loading {
text-align: center;
padding: var(--space-xl);
color: var(--text-muted);
}
/* Tab navigation row */
.wfm-tab-row {
display: flex;
gap: var(--space-sm);
margin-bottom: var(--space-md);
border-bottom: 1px solid var(--border);
padding-bottom: var(--space-sm);
}
.wfm-tab-btn {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
padding: 6px 12px;
}
/* Step list */
.wfm-step-list {
display: flex;
flex-direction: column;
gap: var(--space-sm);
}
/* Step card */
.wfm-step-card {
padding: 12px 16px;
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--bg-secondary);
}
.wfm-step-card-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.wfm-step-card-info {
flex: 1;
min-width: 0;
}
.wfm-step-card-title-row {
display: flex;
align-items: center;
gap: var(--space-sm);
margin-bottom: 4px;
}
.wfm-step-card-name {
font-weight: 600;
font-size: 14px;
}
.wfm-step-card-desc {
font-size: 12px;
color: var(--text-muted);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.wfm-step-card-actions {
display: flex;
gap: 4px;
margin-left: var(--space-sm);
flex-shrink: 0;
}
.wfm-delete-confirm {
display: flex;
gap: 4px;
align-items: center;
}
/* Badges */
.wfm-badge {
font-size: 11px;
padding: 2px 6px;
border-radius: var(--radius-sm);
}
.wfm-badge-enabled {
background: var(--status-success-bg, rgba(34, 197, 94, 0.15));
color: var(--status-success, #22c55e);
}
.wfm-badge-disabled {
background: var(--bg-tertiary);
color: var(--text-muted);
}
.wfm-badge-prompt {
background: rgba(59, 130, 246, 0.15);
color: #3b82f6;
}
.wfm-badge-script {
background: rgba(168, 85, 247, 0.15);
color: #a855f7;
}
.wfm-badge-category {
font-size: 11px;
padding: 2px 6px;
border-radius: var(--radius-sm);
}
/* Template cards */
.wfm-template-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.wfm-template-card {
padding: var(--space-md);
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--bg-secondary);
}
.wfm-template-inner {
display: flex;
gap: 12px;
align-items: flex-start;
}
.wfm-template-icon {
padding: var(--space-sm);
border-radius: var(--radius-sm);
background: var(--bg-tertiary);
color: var(--text);
flex-shrink: 0;
}
.wfm-template-content {
flex: 1;
min-width: 0;
}
.wfm-template-title-row {
display: flex;
align-items: center;
gap: var(--space-sm);
margin-bottom: 4px;
}
.wfm-template-name {
font-weight: 600;
font-size: 14px;
}
.wfm-template-desc {
font-size: 12px;
color: var(--text-muted);
margin-bottom: var(--space-sm);
}
.wfm-template-add-btn {
font-size: 12px;
padding: 4px 12px;
display: flex;
align-items: center;
gap: 4px;
}
/* Edit/create form */
.wfm-form {
padding: var(--space-md);
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--bg-secondary);
}
.wfm-form-title {
margin: 0 0 12px;
font-size: 14px;
font-weight: 600;
}
.wfm-form-fields {
display: flex;
flex-direction: column;
gap: var(--space-md);
}
.wfm-field label,
.wfm-field-label {
display: block;
font-size: 12px;
color: var(--text-muted);
margin-bottom: 4px;
}
.wfm-field input[type="text"],
.wfm-field textarea,
.wfm-field select {
width: 100%;
padding: 8px 12px;
border-radius: var(--radius-md);
border: 1px solid var(--border);
background: var(--bg);
color: var(--text);
font-size: 13px;
font-family: inherit;
outline: none;
transition:
border-color var(--transition-fast),
box-shadow var(--transition-fast);
}
.wfm-field input[type="text"]:focus,
.wfm-field textarea:focus,
.wfm-field select:focus {
border-color: var(--todo);
box-shadow: var(--focus-ring);
}
.wfm-field textarea {
resize: vertical;
}
.wfm-field textarea.wfm-prompt-textarea {
font-family: var(--font-mono, monospace);
}
/* Mode selector */
.wfm-mode-selector {
display: flex;
gap: var(--space-sm);
}
.wfm-mode-btn {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
padding: 6px 12px;
flex: 1;
justify-content: center;
}
/* Prompt header row (label + refine button) */
.wfm-prompt-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
}
.wfm-refine-btn {
font-size: 12px;
display: flex;
align-items: center;
gap: 4px;
}
.wfm-refine-btn span {
font-size: 11px;
}
/* No-scripts placeholder */
.wfm-no-scripts {
padding: 8px 12px;
border-radius: var(--radius-md);
border: 1px solid var(--border);
background: var(--bg-tertiary);
color: var(--text-muted);
font-size: 12px;
}
/* Checkbox toggle */
.wfm-checkbox-label {
display: flex;
align-items: center;
gap: var(--space-sm);
font-size: 13px;
cursor: pointer;
}
/* Form actions */
.wfm-form-actions {
display: flex;
justify-content: flex-end;
gap: var(--space-sm);
margin-top: 4px;
}
/* Footer */
.wfm-footer {
padding: 12px 16px;
border-top: 1px solid var(--border);
}
.wfm-footer-add-btn {
display: flex;
align-items: center;
gap: 6px;
}
/* Spinner */
.wfm-spinner {
margin: 0 auto var(--space-sm);
}
.new-task-modal .form-group small { .new-task-modal .form-group small {
display: block; display: block;
margin-top: var(--space-sm); margin-top: var(--space-sm);