feat(FN-2556): add TaskForm bottom slot for model configuration

- Add a new renderBelowModelConfiguration prop in TaskForm and render it beneath the Model Configuration section inside More options
- Update TaskDetailModal to use the new slot so the Source Issue edit block appears below model settings instead of near the primary section
- Adjust TaskDetailModal source-issue edit tests to expand More options before interacting with source fields
- Add ordering assertions in TaskForm and TaskDetailModal tests to verify the injected block sits between Model Configuration and Workflow Steps
This commit is contained in:
Fusion
2026-04-25 17:29:18 -07:00
committed by gsxdsm
parent ff83adcc5d
commit b6ac77ede7
4 changed files with 73 additions and 1 deletions

View File

@@ -1289,7 +1289,7 @@ export function TaskDetailModal({
onPriorityChange={setEditPriority} onPriorityChange={setEditPriority}
executionMode={editExecutionMode} executionMode={editExecutionMode}
onExecutionModeChange={setEditExecutionMode} onExecutionModeChange={setEditExecutionMode}
renderBelowPrimary={( renderBelowModelConfiguration={(
<div className="form-group detail-source-edit-group"> <div className="form-group detail-source-edit-group">
<label>Source Issue</label> <label>Source Issue</label>
<div className="detail-source-edit-grid"> <div className="detail-source-edit-grid">

View File

@@ -90,6 +90,8 @@ export interface TaskFormProps {
/** Optional content to render between the primary section and the "More options" toggle. */ /** Optional content to render between the primary section and the "More options" toggle. */
renderBelowPrimary?: React.ReactNode; renderBelowPrimary?: React.ReactNode;
/** Optional content to render inside "More options" below Model Configuration. */
renderBelowModelConfiguration?: React.ReactNode;
/** When true, skip rendering the Dependencies form-group inside "More options". Use when the parent renders its own dependency UI via renderBelowPrimary. */ /** When true, skip rendering the Dependencies form-group inside "More options". Use when the parent renders its own dependency UI via renderBelowPrimary. */
hideDependencies?: boolean; hideDependencies?: boolean;
/** When true (default), More options auto-expands when non-default advanced selections are present. */ /** When true (default), More options auto-expands when non-default advanced selections are present. */
@@ -133,6 +135,7 @@ export function TaskForm({
onSubtaskBreakdown, onSubtaskBreakdown,
onClose, onClose,
renderBelowPrimary, renderBelowPrimary,
renderBelowModelConfiguration,
hideDependencies, hideDependencies,
autoExpandMoreOptionsOnSelection = true, autoExpandMoreOptionsOnSelection = true,
reviewLevel, reviewLevel,
@@ -1052,6 +1055,8 @@ export function TaskForm({
)} )}
</div> </div>
{renderBelowModelConfiguration}
{/* Workflow Steps */} {/* Workflow Steps */}
<div className="form-group" data-testid="workflow-steps-section"> <div className="form-group" data-testid="workflow-steps-section">
<label>Workflow Steps</label> <label>Workflow Steps</label>

View File

@@ -4198,6 +4198,7 @@ describe("TaskDetailModal", () => {
); );
fireEvent.click(container.querySelector(".modal-edit-btn")!); fireEvent.click(container.querySelector(".modal-edit-btn")!);
fireEvent.click(screen.getByTestId("task-form-more-options-toggle"));
await waitFor(() => { await waitFor(() => {
expect((screen.getByTestId("task-source-provider-input") as HTMLInputElement).value).toBe("github"); expect((screen.getByTestId("task-source-provider-input") as HTMLInputElement).value).toBe("github");
@@ -4207,6 +4208,45 @@ describe("TaskDetailModal", () => {
expect((screen.getByTestId("task-source-url-input") as HTMLInputElement).value).toBe("https://github.com/runfusion/fusion/issues/2473"); expect((screen.getByTestId("task-source-url-input") as HTMLInputElement).value).toBe("https://github.com/runfusion/fusion/issues/2473");
}); });
it("renders source issue block below Model Configuration in edit mode", async () => {
const { container } = render(
<TaskDetailModal
task={makeTask({
id: "FN-001",
column: "todo",
sourceIssue: {
provider: "github",
repository: "runfusion/fusion",
externalIssueId: "I_kgDOExample",
issueNumber: 2473,
},
})}
onClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
onOpenDetail={noopOpenDetail}
addToast={noop}
/>,
);
fireEvent.click(container.querySelector(".modal-edit-btn")!);
fireEvent.click(screen.getByTestId("task-form-more-options-toggle"));
await waitFor(() => {
const modelLabel = screen.getByText("Model Configuration");
const sourceLabel = screen.getByText("Source Issue");
const workflowSection = screen.getByTestId("workflow-steps-section");
expect(
modelLabel.compareDocumentPosition(sourceLabel) & Node.DOCUMENT_POSITION_FOLLOWING,
).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
expect(
sourceLabel.compareDocumentPosition(workflowSection) & Node.DOCUMENT_POSITION_FOLLOWING,
).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
});
});
it("sends sourceIssue payload when source metadata is edited", async () => { it("sends sourceIssue payload when source metadata is edited", async () => {
const { updateTask } = await import("../../api"); const { updateTask } = await import("../../api");
const mockUpdate = vi.mocked(updateTask); const mockUpdate = vi.mocked(updateTask);
@@ -4235,6 +4275,7 @@ describe("TaskDetailModal", () => {
); );
fireEvent.click(container.querySelector(".modal-edit-btn")!); fireEvent.click(container.querySelector(".modal-edit-btn")!);
fireEvent.click(screen.getByTestId("task-form-more-options-toggle"));
fireEvent.change(screen.getByTestId("task-source-provider-input"), { target: { value: "gitlab" } }); fireEvent.change(screen.getByTestId("task-source-provider-input"), { target: { value: "gitlab" } });
fireEvent.change(screen.getByTestId("task-source-repository-input"), { target: { value: "runfusion/dashboard" } }); fireEvent.change(screen.getByTestId("task-source-repository-input"), { target: { value: "runfusion/dashboard" } });
fireEvent.change(screen.getByTestId("task-source-external-id-input"), { target: { value: "I_kgDONew" } }); fireEvent.change(screen.getByTestId("task-source-external-id-input"), { target: { value: "I_kgDONew" } });
@@ -4283,6 +4324,7 @@ describe("TaskDetailModal", () => {
); );
fireEvent.click(container.querySelector(".modal-edit-btn")!); fireEvent.click(container.querySelector(".modal-edit-btn")!);
fireEvent.click(screen.getByTestId("task-form-more-options-toggle"));
fireEvent.change(screen.getByTestId("task-source-provider-input"), { target: { value: "" } }); fireEvent.change(screen.getByTestId("task-source-provider-input"), { target: { value: "" } });
fireEvent.change(screen.getByTestId("task-source-repository-input"), { target: { value: "" } }); fireEvent.change(screen.getByTestId("task-source-repository-input"), { target: { value: "" } });
fireEvent.change(screen.getByTestId("task-source-external-id-input"), { target: { value: "" } }); fireEvent.change(screen.getByTestId("task-source-external-id-input"), { target: { value: "" } });
@@ -4323,6 +4365,7 @@ describe("TaskDetailModal", () => {
); );
fireEvent.click(container.querySelector(".modal-edit-btn")!); fireEvent.click(container.querySelector(".modal-edit-btn")!);
fireEvent.click(screen.getByTestId("task-form-more-options-toggle"));
fireEvent.change(screen.getByTestId("task-source-provider-input"), { target: { value: "gitlab" } }); fireEvent.change(screen.getByTestId("task-source-provider-input"), { target: { value: "gitlab" } });
fireEvent.click(screen.getByText("Save")); fireEvent.click(screen.getByText("Save"));

View File

@@ -1382,6 +1382,30 @@ describe("TaskForm focus behavior (FN-1459)", () => {
).toBe(Node.DOCUMENT_POSITION_FOLLOWING); ).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
}); });
it("renders renderBelowModelConfiguration content below model section in More options", async () => {
renderTaskForm({
renderBelowModelConfiguration: <div data-testid="injected-below-model">Bottom slot content</div>,
});
const toggle = screen.getByTestId("task-form-more-options-toggle");
fireEvent.click(toggle);
await waitFor(() => {
expect(toggle).toHaveAttribute("aria-expanded", "true");
});
const modelLabel = screen.getByText("Model Configuration");
const workflowLabel = screen.getByText("Workflow Steps");
const injectedBottom = screen.getByTestId("injected-below-model");
expect(
modelLabel.compareDocumentPosition(injectedBottom) & Node.DOCUMENT_POSITION_FOLLOWING,
).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
expect(
injectedBottom.compareDocumentPosition(workflowLabel) & Node.DOCUMENT_POSITION_FOLLOWING,
).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
});
it("hides dependencies section when hideDependencies is true", async () => { it("hides dependencies section when hideDependencies is true", async () => {
renderTaskForm({ hideDependencies: true }); renderTaskForm({ hideDependencies: true });