feat(FN-797): reposition refinement modal primary action near textarea
- Move 'Create Refinement Task' button from modal footer into input group adjacent to feedback textarea - Wrap char count and submit button in new .detail-refine-input-group flex container - Add CSS for input group with space-between layout and gap - Add 3 tests verifying button placement, disabled/enabled rules, and sibling structure - Update dashboard README with refinement modal UX documentation
This commit is contained in:
@@ -47,7 +47,7 @@ AI-guided interactive planning for creating well-specified tasks from high-level
|
||||
- **AI-Assisted Creation Controls**: Plan, Subtask, and Refine buttons appear directly below the description textarea in all task creation surfaces (quick entry box, inline create card, and task form modal). These description-adjacent controls make AI-assisted creation and refinement feel directly associated with the text being edited. Deps, Models, and Save actions remain in the expanded controls footer.
|
||||
- **Layered Model Dropdowns**: Shared model combobox menus render in a top-level portal attached to `document.body`, so they stay above board columns and scrollable modal content instead of being clipped behind surrounding dashboard surfaces.
|
||||
- **Bulk Model Editing**: Update AI model configuration for multiple tasks at once in the list view. Select tasks via checkboxes (archived tasks excluded), then use the "Bulk Edit Models" toolbar to apply executor and/or validator model changes to all selected tasks. Selection persists in localStorage across page reloads.
|
||||
- **Task Details**: View full task specifications, agent logs, and attachments. The Agent Log tab expands to fill the full modal body height above the action bar, providing maximum vertical space for watching live agent output. The tab header shows the effective executor and validator model names resolved from task-level overrides or project/global settings fallbacks, matching the same resolution order the engine uses at runtime.
|
||||
- **Task Details**: View full task specifications, agent logs, and attachments. The Agent Log tab expands to fill the full modal body height above the action bar, providing maximum vertical space for watching live agent output. The tab header shows the effective executor and validator model names resolved from task-level overrides or project/global settings fallbacks, matching the same resolution order the engine uses at runtime. The refinement modal positions the "Create Refinement Task" button adjacent to the feedback textarea alongside the character count, creating a tight input group that connects the submit action directly to the text being edited.
|
||||
- **Changed Files Viewer**: Click a task card's "files changed" button to open a dedicated diff viewer showing only files changed in that task worktree, with per-file statuses and sidebar navigation. On mobile (≤768px), the viewer switches to a single-pane flow: the file list and diff are shown one at a time with a back button for navigation between them
|
||||
- **GitHub Import**: Import issues directly from GitHub repositories
|
||||
- **PR Management**: Create, monitor, and merge pull requests for in-review tasks
|
||||
|
||||
@@ -1195,21 +1195,23 @@ export function TaskDetailModal({
|
||||
maxLength={2000}
|
||||
autoFocus
|
||||
/>
|
||||
<div className="detail-refine-char-count">
|
||||
{refineFeedback.length}/2000 characters
|
||||
<div className="detail-refine-input-group">
|
||||
<div className="detail-refine-char-count">
|
||||
{refineFeedback.length}/2000 characters
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-primary btn-sm"
|
||||
onClick={handleSubmitRefine}
|
||||
disabled={!refineFeedback.trim() || isRefining}
|
||||
>
|
||||
{isRefining ? "Creating..." : "Create Refinement Task"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-actions">
|
||||
<button className="btn btn-sm" onClick={handleCloseRefineModal} disabled={isRefining}>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary btn-sm"
|
||||
onClick={handleSubmitRefine}
|
||||
disabled={!refineFeedback.trim() || isRefining}
|
||||
>
|
||||
{isRefining ? "Creating..." : "Create Refinement Task"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2787,6 +2787,82 @@ describe("TaskDetailModal", () => {
|
||||
expect(addToast).toHaveBeenCalledWith("Task must be in 'done' or 'in-review' column", "error");
|
||||
});
|
||||
});
|
||||
|
||||
it("renders submit button inside the input group adjacent to textarea", () => {
|
||||
const { container } = render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ id: "FN-001", column: "done" })}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText("Refine"));
|
||||
|
||||
// The submit button should be inside .detail-refine-input-group (the input area)
|
||||
const inputGroup = container.querySelector(".detail-refine-input-group");
|
||||
expect(inputGroup).toBeTruthy();
|
||||
const submitButton = inputGroup!.querySelector("button.btn-primary");
|
||||
expect(submitButton).toBeTruthy();
|
||||
expect(submitButton!.textContent).toBe("Create Refinement Task");
|
||||
|
||||
// The submit button should NOT be in the footer .modal-actions
|
||||
const modalActions = container.querySelector(".detail-refine-modal .modal-actions");
|
||||
expect(modalActions).toBeTruthy();
|
||||
expect(modalActions!.querySelector("button.btn-primary")).toBeNull();
|
||||
});
|
||||
|
||||
it("submit button in input group follows the same disabled/enabled rules", async () => {
|
||||
render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ id: "FN-001", column: "done" })}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText("Refine"));
|
||||
|
||||
// Submit button starts disabled (no feedback)
|
||||
const submitButton = screen.getByText("Create Refinement Task");
|
||||
expect(submitButton.hasAttribute("disabled")).toBe(true);
|
||||
|
||||
// Enter feedback to enable it
|
||||
const textarea = screen.getByPlaceholderText("Enter your feedback here...");
|
||||
await act(async () => {
|
||||
fireEvent.change(textarea, { target: { value: "Some feedback" } });
|
||||
});
|
||||
|
||||
expect(submitButton.hasAttribute("disabled")).toBe(false);
|
||||
});
|
||||
|
||||
it("character count and submit button are siblings in the input group", () => {
|
||||
const { container } = render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ id: "FN-001", column: "done" })}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText("Refine"));
|
||||
|
||||
const inputGroup = container.querySelector(".detail-refine-input-group")!;
|
||||
expect(inputGroup.querySelector(".detail-refine-char-count")).toBeTruthy();
|
||||
expect(inputGroup.querySelector("button.btn-primary")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("inline editing", () => {
|
||||
|
||||
@@ -3335,9 +3335,15 @@ body {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.detail-refine-char-count {
|
||||
.detail-refine-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: var(--space-sm);
|
||||
text-align: right;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.detail-refine-char-count {
|
||||
font-size: 12px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user