feat(FN-1103): improve configured workflow steps experience
- Render a dedicated configured-steps state in WorkflowResultsTab with step count, descriptions, and phase badges when results are not yet available - Keep inline workflow step editing available from the configured state and results state, including add/remove and execution-order controls - Refine workflow step loading/edit behavior by aligning callback names and fetching step definitions by project context - Add dashboard styles for the configured workflow step cards and improved edit toggle button treatment - Expand WorkflowResultsTab and TaskDetailModal tests and add electron-updater type declarations to restore workspace build
This commit is contained in:
@@ -169,17 +169,46 @@ describe("WorkflowResultsTab", () => {
|
|||||||
expect(screen.getByText("No workflow steps configured for this task.")).toBeInTheDocument();
|
expect(screen.getByText("No workflow steps configured for this task.")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows 'configured but not run' empty state when enabledWorkflowSteps is non-empty", () => {
|
it("shows configured step details when enabledWorkflowSteps is non-empty and results are empty", async () => {
|
||||||
render(
|
render(
|
||||||
<WorkflowResultsTab
|
<WorkflowResultsTab
|
||||||
taskId="FN-001"
|
taskId="FN-001"
|
||||||
results={[]}
|
results={[]}
|
||||||
enabledWorkflowSteps={["WS-001", "WS-002"]}
|
enabledWorkflowSteps={["WS-101", "WS-102"]}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByTestId("workflow-results-empty")).toBeInTheDocument();
|
expect(screen.getByTestId("workflow-configured-steps")).toBeInTheDocument();
|
||||||
expect(screen.getByText("Workflow steps configured but haven't run yet.")).toBeInTheDocument();
|
expect(screen.getByTestId("workflow-configured-header")).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId("workflow-configured-count")).toHaveTextContent("2 steps");
|
||||||
|
|
||||||
|
const qaStep = await screen.findByTestId("workflow-configured-step-WS-101");
|
||||||
|
const docsStep = await screen.findByTestId("workflow-configured-step-WS-102");
|
||||||
|
|
||||||
|
expect(qaStep).toHaveTextContent("QA Check");
|
||||||
|
expect(qaStep).toHaveTextContent("Run test suite");
|
||||||
|
expect(screen.getByTestId("workflow-configured-phase-WS-101")).toHaveTextContent("Pre-merge");
|
||||||
|
|
||||||
|
expect(docsStep).toHaveTextContent("Docs Review");
|
||||||
|
expect(docsStep).toHaveTextContent("Review docs");
|
||||||
|
expect(screen.getByTestId("workflow-configured-phase-WS-102")).toHaveTextContent("Post-merge");
|
||||||
|
|
||||||
|
expect(screen.getByText("Pre-merge steps run after implementation, before merge. Post-merge steps run after merge succeeds.")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to step ID and default description when definition is missing", () => {
|
||||||
|
render(
|
||||||
|
<WorkflowResultsTab
|
||||||
|
taskId="FN-001"
|
||||||
|
results={[]}
|
||||||
|
enabledWorkflowSteps={["WS-unknown"]}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const fallbackStep = screen.getByTestId("workflow-configured-step-WS-unknown");
|
||||||
|
expect(fallbackStep).toHaveTextContent("WS-unknown");
|
||||||
|
expect(fallbackStep).toHaveTextContent("Step definition not found.");
|
||||||
|
expect(screen.getByTestId("workflow-configured-phase-WS-unknown")).toHaveTextContent("Pre-merge");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows loading state when loading prop is true", () => {
|
it("shows loading state when loading prop is true", () => {
|
||||||
@@ -363,22 +392,43 @@ describe("WorkflowResultsTab", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("workflow step editing", () => {
|
describe("workflow step editing", () => {
|
||||||
it("shows edit button when canEdit is true", () => {
|
it("shows edit button when canEdit is true and configured steps are present", () => {
|
||||||
render(<WorkflowResultsTab taskId="FN-001" results={[]} canEdit />);
|
render(
|
||||||
|
<WorkflowResultsTab
|
||||||
|
taskId="FN-001"
|
||||||
|
results={[]}
|
||||||
|
canEdit
|
||||||
|
enabledWorkflowSteps={["WS-101"]}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
expect(screen.getByTestId("workflow-steps-edit-toggle")).toBeInTheDocument();
|
expect(screen.getByTestId("workflow-steps-edit-toggle")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not show edit button when canEdit is false or undefined", () => {
|
it("does not show edit button when canEdit is false or undefined", () => {
|
||||||
const { rerender } = render(<WorkflowResultsTab taskId="FN-001" results={[]} canEdit={false} />);
|
const { rerender } = render(
|
||||||
|
<WorkflowResultsTab
|
||||||
|
taskId="FN-001"
|
||||||
|
results={[]}
|
||||||
|
canEdit={false}
|
||||||
|
enabledWorkflowSteps={["WS-101"]}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
expect(screen.queryByTestId("workflow-steps-edit-toggle")).not.toBeInTheDocument();
|
expect(screen.queryByTestId("workflow-steps-edit-toggle")).not.toBeInTheDocument();
|
||||||
|
|
||||||
rerender(<WorkflowResultsTab taskId="FN-001" results={[]} />);
|
rerender(<WorkflowResultsTab taskId="FN-001" results={[]} enabledWorkflowSteps={["WS-101"]} />);
|
||||||
expect(screen.queryByTestId("workflow-steps-edit-toggle")).not.toBeInTheDocument();
|
expect(screen.queryByTestId("workflow-steps-edit-toggle")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows and hides workflow step checkboxes when edit is toggled", async () => {
|
it("shows and hides workflow step checkboxes when edit is toggled", async () => {
|
||||||
render(<WorkflowResultsTab taskId="FN-001" results={[]} canEdit />);
|
render(
|
||||||
|
<WorkflowResultsTab
|
||||||
|
taskId="FN-001"
|
||||||
|
results={[]}
|
||||||
|
canEdit
|
||||||
|
enabledWorkflowSteps={["WS-101"]}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
expect(screen.queryByTestId("workflow-steps-editor")).not.toBeInTheDocument();
|
expect(screen.queryByTestId("workflow-steps-editor")).not.toBeInTheDocument();
|
||||||
|
|
||||||
@@ -399,7 +449,7 @@ describe("WorkflowResultsTab", () => {
|
|||||||
taskId="FN-001"
|
taskId="FN-001"
|
||||||
results={[]}
|
results={[]}
|
||||||
canEdit
|
canEdit
|
||||||
enabledWorkflowSteps={[]}
|
enabledWorkflowSteps={["WS-102"]}
|
||||||
onWorkflowStepsChange={onWorkflowStepsChange}
|
onWorkflowStepsChange={onWorkflowStepsChange}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
@@ -408,7 +458,7 @@ describe("WorkflowResultsTab", () => {
|
|||||||
const stepCheckbox = (await screen.findByTestId("workflow-step-checkbox-WS-101")).querySelector("input") as HTMLInputElement;
|
const stepCheckbox = (await screen.findByTestId("workflow-step-checkbox-WS-101")).querySelector("input") as HTMLInputElement;
|
||||||
fireEvent.click(stepCheckbox);
|
fireEvent.click(stepCheckbox);
|
||||||
|
|
||||||
expect(onWorkflowStepsChange).toHaveBeenCalledWith(["WS-101"]);
|
expect(onWorkflowStepsChange).toHaveBeenCalledWith(["WS-102", "WS-101"]);
|
||||||
|
|
||||||
onWorkflowStepsChange.mockClear();
|
onWorkflowStepsChange.mockClear();
|
||||||
rerender(
|
rerender(
|
||||||
@@ -421,6 +471,10 @@ describe("WorkflowResultsTab", () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!screen.queryByTestId("workflow-steps-editor")) {
|
||||||
|
fireEvent.click(screen.getByTestId("workflow-steps-edit-toggle"));
|
||||||
|
}
|
||||||
|
|
||||||
const selectedCheckbox = (await screen.findByTestId("workflow-step-checkbox-WS-101")).querySelector("input") as HTMLInputElement;
|
const selectedCheckbox = (await screen.findByTestId("workflow-step-checkbox-WS-101")).querySelector("input") as HTMLInputElement;
|
||||||
expect(selectedCheckbox.checked).toBe(true);
|
expect(selectedCheckbox.checked).toBe(true);
|
||||||
fireEvent.click(selectedCheckbox);
|
fireEvent.click(selectedCheckbox);
|
||||||
|
|||||||
@@ -108,11 +108,6 @@ export function WorkflowResultsTab({
|
|||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!canEdit) {
|
|
||||||
setAllWorkflowSteps([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
fetchWorkflowSteps(projectId)
|
fetchWorkflowSteps(projectId)
|
||||||
.then((steps) => {
|
.then((steps) => {
|
||||||
@@ -129,7 +124,7 @@ export function WorkflowResultsTab({
|
|||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [canEdit, projectId]);
|
}, [projectId]);
|
||||||
|
|
||||||
const selectedWorkflowSteps = enabledWorkflowSteps ?? [];
|
const selectedWorkflowSteps = enabledWorkflowSteps ?? [];
|
||||||
|
|
||||||
@@ -160,7 +155,7 @@ export function WorkflowResultsTab({
|
|||||||
setExpandedOutputs((prev) => ({ ...prev, [stepId]: !prev[stepId] }));
|
setExpandedOutputs((prev) => ({ ...prev, [stepId]: !prev[stepId] }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleWorkflowStep = useCallback((stepId: string, checked: boolean) => {
|
const toggleStep = useCallback((stepId: string, checked: boolean) => {
|
||||||
if (!onWorkflowStepsChange) return;
|
if (!onWorkflowStepsChange) return;
|
||||||
|
|
||||||
if (checked) {
|
if (checked) {
|
||||||
@@ -195,6 +190,114 @@ export function WorkflowResultsTab({
|
|||||||
}, [onWorkflowStepsChange, selectedWorkflowSteps]);
|
}, [onWorkflowStepsChange, selectedWorkflowSteps]);
|
||||||
|
|
||||||
const hasResults = results.length > 0;
|
const hasResults = results.length > 0;
|
||||||
|
const hasConfiguredSteps = selectedWorkflowSteps.length > 0;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!canEdit) {
|
||||||
|
setIsEditing(false);
|
||||||
|
}
|
||||||
|
}, [canEdit]);
|
||||||
|
|
||||||
|
const configuredSteps = useMemo(() => {
|
||||||
|
return selectedWorkflowSteps.map((stepId) => {
|
||||||
|
const stepInfo = workflowStepLookup.get(stepId);
|
||||||
|
return {
|
||||||
|
id: stepId,
|
||||||
|
name: stepInfo?.name || stepId,
|
||||||
|
description: stepInfo?.description || "Step definition not found.",
|
||||||
|
phase: stepInfo?.phase || "pre-merge",
|
||||||
|
} as WorkflowStepOption;
|
||||||
|
});
|
||||||
|
}, [selectedWorkflowSteps, workflowStepLookup]);
|
||||||
|
|
||||||
|
const renderEditor = () => {
|
||||||
|
if (!canEdit || !isEditing || loading) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="workflow-results-editor" data-testid="workflow-steps-editor">
|
||||||
|
<small style={{ marginBottom: "8px", display: "block" }}>
|
||||||
|
Select steps to run after task implementation completes
|
||||||
|
</small>
|
||||||
|
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
|
||||||
|
{workflowStepOptions.map((step) => (
|
||||||
|
<label
|
||||||
|
key={step.id}
|
||||||
|
className="checkbox-label"
|
||||||
|
style={{ display: "flex", alignItems: "flex-start", gap: "8px" }}
|
||||||
|
data-testid={step.id === "browser-verification"
|
||||||
|
? "browser-verification-checkbox"
|
||||||
|
: `workflow-step-checkbox-${step.id}`}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={selectedWorkflowSteps.includes(step.id)}
|
||||||
|
onChange={(event) => toggleStep(step.id, event.target.checked)}
|
||||||
|
style={{ marginTop: "2px" }}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<span style={{ fontWeight: 500, fontSize: "13px" }}>
|
||||||
|
{step.name}
|
||||||
|
{phaseBadge(step.phase, step.id, "workflow-step-phase")}
|
||||||
|
</span>
|
||||||
|
<div style={{ fontSize: "12px", color: "var(--text-secondary)", marginTop: "2px" }}>
|
||||||
|
{step.description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedWorkflowSteps.length > 1 && (
|
||||||
|
<div className="workflow-step-order" data-testid="workflow-step-order">
|
||||||
|
<small className="workflow-step-order-label">Execution order:</small>
|
||||||
|
{selectedWorkflowSteps.map((stepId, index) => {
|
||||||
|
const stepInfo = workflowStepLookup.get(stepId);
|
||||||
|
return (
|
||||||
|
<div key={stepId} className="workflow-step-order-item" data-testid={`workflow-step-order-item-${stepId}`}>
|
||||||
|
<span className="workflow-step-order-number">{index + 1}</span>
|
||||||
|
<span className="workflow-step-order-name">{stepInfo?.name || stepId}</span>
|
||||||
|
<div className="workflow-step-order-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-icon btn-sm"
|
||||||
|
onClick={() => moveWorkflowStepUp(index)}
|
||||||
|
disabled={index === 0}
|
||||||
|
data-testid={`workflow-step-move-up-${stepId}`}
|
||||||
|
title="Move up"
|
||||||
|
>
|
||||||
|
<ChevronUp size={14} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-icon btn-sm"
|
||||||
|
onClick={() => moveWorkflowStepDown(index)}
|
||||||
|
disabled={index === selectedWorkflowSteps.length - 1}
|
||||||
|
data-testid={`workflow-step-move-down-${stepId}`}
|
||||||
|
title="Move down"
|
||||||
|
>
|
||||||
|
<ChevronDown size={14} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-icon btn-sm"
|
||||||
|
onClick={() => removeWorkflowStep(stepId)}
|
||||||
|
data-testid={`workflow-step-remove-${stepId}`}
|
||||||
|
title="Remove"
|
||||||
|
>
|
||||||
|
<X size={14} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const renderResults = () => {
|
const renderResults = () => {
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -207,14 +310,9 @@ export function WorkflowResultsTab({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!hasResults) {
|
if (!hasResults) {
|
||||||
const hasConfiguredSteps = selectedWorkflowSteps.length > 0;
|
|
||||||
return (
|
return (
|
||||||
<div className="workflow-results-empty" data-testid="workflow-results-empty">
|
<div className="workflow-results-empty" data-testid="workflow-results-empty">
|
||||||
<p>
|
<p>No workflow steps configured for this task.</p>
|
||||||
{hasConfiguredSteps
|
|
||||||
? "Workflow steps configured but haven't run yet."
|
|
||||||
: "No workflow steps configured for this task."}
|
|
||||||
</p>
|
|
||||||
<p className="workflow-results-empty-hint">
|
<p className="workflow-results-empty-hint">
|
||||||
Pre-merge steps run after implementation, before merge. Post-merge steps run after merge succeeds.
|
Pre-merge steps run after implementation, before merge. Post-merge steps run after merge succeeds.
|
||||||
</p>
|
</p>
|
||||||
@@ -310,109 +408,79 @@ export function WorkflowResultsTab({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const showEditUI = !!canEdit && isEditing;
|
const editButton = canEdit ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="modal-edit-btn workflow-results-edit-toggle"
|
||||||
|
onClick={() => setIsEditing((prev) => !prev)}
|
||||||
|
data-testid="workflow-steps-edit-toggle"
|
||||||
|
aria-label={isEditing ? "Done editing workflow steps" : "Edit workflow steps"}
|
||||||
|
title={isEditing ? "Done" : "Edit"}
|
||||||
|
>
|
||||||
|
{isEditing ? (
|
||||||
|
<>
|
||||||
|
<Check size={14} />
|
||||||
|
Done
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Pencil size={14} />
|
||||||
|
Edit
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
const showConfiguredStepsState = !loading && !hasResults && hasConfiguredSteps;
|
||||||
|
const showEditHeaderForResults = canEdit && hasResults;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="workflow-results-tab" data-task-id={taskId}>
|
<div className="workflow-results-tab" data-task-id={taskId}>
|
||||||
{canEdit && (
|
{showConfiguredStepsState ? (
|
||||||
<div className="workflow-results-edit-header" data-testid="workflow-results-edit-header">
|
<div className="workflow-configured-steps" data-testid="workflow-configured-steps">
|
||||||
<h4>Workflow Steps</h4>
|
<div className="workflow-configured-header" data-testid="workflow-configured-header">
|
||||||
<button
|
<div className="workflow-configured-title-row">
|
||||||
type="button"
|
<h4>Configured Workflow Steps</h4>
|
||||||
className="modal-edit-btn"
|
<span className="workflow-configured-count" data-testid="workflow-configured-count">
|
||||||
onClick={() => setIsEditing((prev) => !prev)}
|
{configuredSteps.length} step{configuredSteps.length === 1 ? "" : "s"}
|
||||||
data-testid="workflow-steps-edit-toggle"
|
</span>
|
||||||
aria-label={isEditing ? "Done editing workflow steps" : "Edit workflow steps"}
|
</div>
|
||||||
title={isEditing ? "Done" : "Edit"}
|
{editButton}
|
||||||
>
|
</div>
|
||||||
{isEditing ? <Check size={14} /> : <Pencil size={14} />}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{(!showEditUI || hasResults || loading) && renderResults()}
|
<div className="workflow-configured-list" data-testid="workflow-configured-list">
|
||||||
|
{configuredSteps.map((step) => (
|
||||||
{showEditUI && !loading && (
|
<div
|
||||||
<div className="workflow-results-editor" data-testid="workflow-steps-editor">
|
|
||||||
<small style={{ marginBottom: "8px", display: "block" }}>
|
|
||||||
Select steps to run after task implementation completes
|
|
||||||
</small>
|
|
||||||
|
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
|
|
||||||
{workflowStepOptions.map((step) => (
|
|
||||||
<label
|
|
||||||
key={step.id}
|
key={step.id}
|
||||||
className="checkbox-label"
|
className="workflow-configured-item"
|
||||||
style={{ display: "flex", alignItems: "flex-start", gap: "8px" }}
|
data-testid={`workflow-configured-step-${step.id}`}
|
||||||
data-testid={step.id === "browser-verification"
|
|
||||||
? "browser-verification-checkbox"
|
|
||||||
: `workflow-step-checkbox-${step.id}`}
|
|
||||||
>
|
>
|
||||||
<input
|
<div className="workflow-configured-name">
|
||||||
type="checkbox"
|
{step.name}
|
||||||
checked={selectedWorkflowSteps.includes(step.id)}
|
{phaseBadge(step.phase, step.id, "workflow-configured-phase")}
|
||||||
onChange={(event) => toggleWorkflowStep(step.id, event.target.checked)}
|
|
||||||
style={{ marginTop: "2px" }}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<span style={{ fontWeight: 500, fontSize: "13px" }}>
|
|
||||||
{step.name}
|
|
||||||
{phaseBadge(step.phase, step.id, "workflow-step-phase")}
|
|
||||||
</span>
|
|
||||||
<div style={{ fontSize: "12px", color: "var(--text-secondary)", marginTop: "2px" }}>
|
|
||||||
{step.description}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</label>
|
<p className="workflow-configured-description">{step.description}</p>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selectedWorkflowSteps.length > 1 && (
|
<p className="workflow-results-empty-hint">
|
||||||
<div className="workflow-step-order" data-testid="workflow-step-order">
|
Pre-merge steps run after implementation, before merge. Post-merge steps run after merge succeeds.
|
||||||
<small className="workflow-step-order-label">Execution order:</small>
|
</p>
|
||||||
{selectedWorkflowSteps.map((stepId, index) => {
|
|
||||||
const stepInfo = workflowStepLookup.get(stepId);
|
{renderEditor()}
|
||||||
return (
|
</div>
|
||||||
<div key={stepId} className="workflow-step-order-item" data-testid={`workflow-step-order-item-${stepId}`}>
|
) : (
|
||||||
<span className="workflow-step-order-number">{index + 1}</span>
|
<>
|
||||||
<span className="workflow-step-order-name">{stepInfo?.name || stepId}</span>
|
{showEditHeaderForResults && (
|
||||||
<div className="workflow-step-order-actions">
|
<div className="workflow-results-edit-header" data-testid="workflow-results-edit-header">
|
||||||
<button
|
<h4>Workflow Steps</h4>
|
||||||
type="button"
|
{editButton}
|
||||||
className="btn btn-icon btn-sm"
|
|
||||||
onClick={() => moveWorkflowStepUp(index)}
|
|
||||||
disabled={index === 0}
|
|
||||||
data-testid={`workflow-step-move-up-${stepId}`}
|
|
||||||
title="Move up"
|
|
||||||
>
|
|
||||||
<ChevronUp size={14} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-icon btn-sm"
|
|
||||||
onClick={() => moveWorkflowStepDown(index)}
|
|
||||||
disabled={index === selectedWorkflowSteps.length - 1}
|
|
||||||
data-testid={`workflow-step-move-down-${stepId}`}
|
|
||||||
title="Move down"
|
|
||||||
>
|
|
||||||
<ChevronDown size={14} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-icon btn-sm"
|
|
||||||
onClick={() => removeWorkflowStep(stepId)}
|
|
||||||
data-testid={`workflow-step-remove-${stepId}`}
|
|
||||||
title="Remove"
|
|
||||||
>
|
|
||||||
<X size={14} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
{renderResults()}
|
||||||
|
{renderEditor()}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4241,7 +4241,7 @@ describe("TaskDetailModal", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders empty state when workflow results are empty", async () => {
|
it("renders configured workflow steps state when results are empty", async () => {
|
||||||
const { fetchWorkflowResults } = await import("../../api");
|
const { fetchWorkflowResults } = await import("../../api");
|
||||||
const mockFetch = vi.mocked(fetchWorkflowResults);
|
const mockFetch = vi.mocked(fetchWorkflowResults);
|
||||||
mockFetch.mockResolvedValueOnce([]);
|
mockFetch.mockResolvedValueOnce([]);
|
||||||
@@ -4261,8 +4261,8 @@ describe("TaskDetailModal", () => {
|
|||||||
fireEvent.click(screen.getByText("Workflow"));
|
fireEvent.click(screen.getByText("Workflow"));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByTestId("workflow-results-empty")).toBeTruthy();
|
expect(screen.getByTestId("workflow-configured-steps")).toBeTruthy();
|
||||||
expect(screen.getByText("Workflow steps configured but haven't run yet.")).toBeTruthy();
|
expect(screen.getByTestId("workflow-configured-step-WS-001")).toHaveTextContent("WS-001");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -19236,6 +19236,92 @@ html .column.drag-over * {
|
|||||||
color: var(--text-primary, #e6edf3);
|
color: var(--text-primary, #e6edf3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workflow-results-edit-toggle {
|
||||||
|
width: auto;
|
||||||
|
height: 30px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border: 1px solid var(--border, #30363d);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--surface-elevated, #161b22);
|
||||||
|
color: var(--text-muted, #8b949e);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-results-edit-toggle:hover {
|
||||||
|
background: var(--surface, #21262d);
|
||||||
|
color: var(--text-primary, #e6edf3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-results-edit-toggle svg {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-configured-steps {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-sm, 8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-configured-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-sm, 8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-configured-title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-sm, 8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-configured-title-row h4 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-primary, #e6edf3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-configured-count {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid var(--border, #30363d);
|
||||||
|
color: var(--text-muted, #8b949e);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-configured-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-sm, 8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-configured-item {
|
||||||
|
border: 1px solid var(--border, #30363d);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: var(--space-md, 16px);
|
||||||
|
background: var(--surface-elevated, #161b22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-configured-name {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-primary, #e6edf3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-configured-description {
|
||||||
|
margin: var(--space-xs, 4px) 0 0;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: var(--text-secondary, #c9d1d9);
|
||||||
|
}
|
||||||
|
|
||||||
.workflow-results-editor {
|
.workflow-results-editor {
|
||||||
border: 1px solid var(--border, #30363d);
|
border: 1px solid var(--border, #30363d);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|||||||
17
packages/desktop/src/electron-updater.d.ts
vendored
Normal file
17
packages/desktop/src/electron-updater.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
declare module "electron-updater" {
|
||||||
|
export interface UpdateInfo {
|
||||||
|
version?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AutoUpdater {
|
||||||
|
autoDownload: boolean;
|
||||||
|
autoInstallOnAppQuit: boolean;
|
||||||
|
on(event: "update-available", listener: (info: UpdateInfo) => void): this;
|
||||||
|
on(event: "update-downloaded", listener: (info: UpdateInfo) => void): this;
|
||||||
|
on(event: "error", listener: (error: Error) => void): this;
|
||||||
|
checkForUpdates(): Promise<unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const autoUpdater: AutoUpdater;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user