fix(FN-2498): keep New Task more options collapsed by default

- Add an autoExpandMoreOptionsOnSelection prop to TaskForm and gate advanced-section auto-expansion behind it
- Disable auto-expansion in NewTaskModal so default-on workflow step preselection does not open More options on first render
- Mark collapsed advanced content with the hidden attribute to prevent interaction until expanded
- Add dashboard tests for default collapsed state, opt-out auto-expand behavior, and default-on workflow-step regression coverage
- Replace hardcoded spacing/transition values in NewTaskModal.css with design tokens
This commit is contained in:
Fusion
2026-04-25 11:21:35 -07:00
committed by gsxdsm
parent a8124da1ff
commit 79b73c343c
5 changed files with 93 additions and 16 deletions

View File

@@ -4,7 +4,7 @@
} }
.new-task-modal .modal-body { .new-task-modal .modal-body {
padding: 20px 24px; padding: var(--space-xl);
overflow-y: auto; overflow-y: auto;
max-height: calc(80vh - 120px); max-height: calc(80vh - 120px);
flex: 1; flex: 1;
@@ -13,7 +13,7 @@
.new-task-modal .form-group { .new-task-modal .form-group {
margin-top: 0; margin-top: 0;
margin-bottom: 20px; margin-bottom: var(--space-xl);
padding: 0; padding: 0;
} }
@@ -26,7 +26,7 @@
} }
.new-task-modal .checkbox-label { .new-task-modal .checkbox-label {
margin-bottom: 4px !important; margin-bottom: var(--space-xs) !important;
} }
.task-form-primary-section { .task-form-primary-section {
@@ -72,7 +72,7 @@
width: fit-content; width: fit-content;
margin-top: 0; margin-top: 0;
margin-bottom: 0; margin-bottom: 0;
padding: 6px; padding: var(--space-sm);
border: 1px solid var(--border-subtle); border: 1px solid var(--border-subtle);
border-radius: var(--radius-md); border-radius: var(--radius-md);
background: var(--bg-tertiary); background: var(--bg-tertiary);
@@ -88,7 +88,7 @@
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: var(--space-sm); gap: var(--space-sm);
padding: 10px 2px; padding: var(--space-sm) var(--space-xs);
margin: 0 0 var(--space-sm); margin: 0 0 var(--space-sm);
border: none; border: none;
border-top: 1px solid var(--border-subtle); border-top: 1px solid var(--border-subtle);
@@ -118,18 +118,18 @@
} }
.task-form-more-options { .task-form-more-options {
margin-left: 6px; margin-left: var(--space-sm);
padding-left: 12px; padding-left: var(--space-md);
border-left: 1px solid var(--border-subtle); border-left: 1px solid var(--border-subtle);
overflow: hidden; overflow: hidden;
max-height: 2400px; max-height: 2400px;
opacity: 1; opacity: 1;
transition: transition:
max-height 0.24s ease, max-height var(--transition-slow),
opacity 0.2s ease, opacity var(--transition-normal),
margin-top 0.2s ease, margin-top var(--transition-normal),
padding 0.2s ease, padding var(--transition-normal),
border-color 0.2s ease; border-color var(--transition-normal);
} }
.task-form-more-options.collapsed { .task-form-more-options.collapsed {
@@ -157,7 +157,7 @@
} }
.task-form-more-options .form-group small { .task-form-more-options .form-group small {
margin-top: 6px; margin-top: var(--space-sm);
color: var(--text-secondary, var(--text-muted)); color: var(--text-secondary, var(--text-muted));
} }
@@ -267,7 +267,7 @@
.workflow-step-order-actions { .workflow-step-order-actions {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 2px; gap: var(--space-xs);
flex-shrink: 0; flex-shrink: 0;
} }

View File

@@ -455,6 +455,7 @@ export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask,
onPriorityChange={setPriority} onPriorityChange={setPriority}
renderBelowPrimary={quickFields} renderBelowPrimary={quickFields}
hideDependencies={true} hideDependencies={true}
autoExpandMoreOptionsOnSelection={false}
/> />
</div> </div>

View File

@@ -88,6 +88,8 @@ export interface TaskFormProps {
renderBelowPrimary?: React.ReactNode; renderBelowPrimary?: 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. */
autoExpandMoreOptionsOnSelection?: boolean;
} }
export function TaskForm({ export function TaskForm({
@@ -128,6 +130,7 @@ export function TaskForm({
onClose, onClose,
renderBelowPrimary, renderBelowPrimary,
hideDependencies, hideDependencies,
autoExpandMoreOptionsOnSelection = true,
reviewLevel, reviewLevel,
onReviewLevelChange, onReviewLevelChange,
}: TaskFormProps) { }: TaskFormProps) {
@@ -144,7 +147,9 @@ export function TaskForm({
reviewLevel !== undefined; reviewLevel !== undefined;
const [showDepDropdown, setShowDepDropdown] = useState(false); const [showDepDropdown, setShowDepDropdown] = useState(false);
const [showMoreOptions, setShowMoreOptions] = useState(hasInitialMoreOptions); const [showMoreOptions, setShowMoreOptions] = useState(
autoExpandMoreOptionsOnSelection ? hasInitialMoreOptions : false,
);
const [depSearch, setDepSearch] = useState(""); const [depSearch, setDepSearch] = useState("");
const [availableModels, setAvailableModels] = useState<ModelInfo[]>([]); const [availableModels, setAvailableModels] = useState<ModelInfo[]>([]);
const [favoriteProviders, setFavoriteProviders] = useState<string[]>([]); const [favoriteProviders, setFavoriteProviders] = useState<string[]>([]);
@@ -243,11 +248,16 @@ export function TaskForm({
// Auto-expand advanced options when non-default values are present. // Auto-expand advanced options when non-default values are present.
useEffect(() => { useEffect(() => {
if (!autoExpandMoreOptionsOnSelection) {
hadMoreOptionSelectionsRef.current = hasMoreOptionSelections;
return;
}
if (hasMoreOptionSelections && !hadMoreOptionSelectionsRef.current) { if (hasMoreOptionSelections && !hadMoreOptionSelectionsRef.current) {
setShowMoreOptions(true); setShowMoreOptions(true);
} }
hadMoreOptionSelectionsRef.current = hasMoreOptionSelections; hadMoreOptionSelectionsRef.current = hasMoreOptionSelections;
}, [hasMoreOptionSelections]); }, [hasMoreOptionSelections, autoExpandMoreOptionsOnSelection]);
// Keep dependency dropdown state clean when advanced options are collapsed. // Keep dependency dropdown state clean when advanced options are collapsed.
useEffect(() => { useEffect(() => {
@@ -731,6 +741,7 @@ export function TaskForm({
id="task-form-more-options" id="task-form-more-options"
className={`task-form-more-options${showMoreOptions ? "" : " collapsed"}`} className={`task-form-more-options${showMoreOptions ? "" : " collapsed"}`}
aria-hidden={!showMoreOptions} aria-hidden={!showMoreOptions}
hidden={!showMoreOptions}
data-testid="task-form-more-options" data-testid="task-form-more-options"
> >
{/* Attachments */} {/* Attachments */}

View File

@@ -97,7 +97,9 @@ describe("NewTaskModal", () => {
renderNewTaskModal(); renderNewTaskModal();
const toggle = screen.getByTestId("task-form-more-options-toggle"); const toggle = screen.getByTestId("task-form-more-options-toggle");
const moreOptions = screen.getByTestId("task-form-more-options");
expect(toggle).toHaveAttribute("aria-expanded", "false"); expect(toggle).toHaveAttribute("aria-expanded", "false");
expect(moreOptions).toHaveAttribute("hidden");
// Dependencies are now in quick-fields (visible by default), so the dep-trigger is present // Dependencies are now in quick-fields (visible by default), so the dep-trigger is present
expect(screen.getByTestId("dep-trigger")).toBeInTheDocument(); expect(screen.getByTestId("dep-trigger")).toBeInTheDocument();
@@ -105,6 +107,7 @@ describe("NewTaskModal", () => {
await waitFor(() => { await waitFor(() => {
expect(toggle).toHaveAttribute("aria-expanded", "true"); expect(toggle).toHaveAttribute("aria-expanded", "true");
expect(moreOptions).not.toHaveAttribute("hidden");
}); });
// Model Configuration, Attachments, and Workflow Steps are revealed // Model Configuration, Attachments, and Workflow Steps are revealed
expect(screen.getByText(/Model Configuration/i)).toBeTruthy(); expect(screen.getByText(/Model Configuration/i)).toBeTruthy();
@@ -536,6 +539,32 @@ describe("NewTaskModal", () => {
// DefaultOn workflow step handling (FN-883) // DefaultOn workflow step handling (FN-883)
describe("defaultOn workflow step handling", () => { describe("defaultOn workflow step handling", () => {
it("keeps More options collapsed by default even when defaultOn workflow steps are auto-applied", async () => {
const { fetchWorkflowSteps } = await import("../../api");
vi.mocked(fetchWorkflowSteps).mockResolvedValueOnce([
{ id: "WS-001", name: "QA Check", description: "Run tests", prompt: "Check tests", enabled: true, defaultOn: true, createdAt: "", updatedAt: "" },
]);
renderNewTaskModal();
await waitFor(() => {
expect(screen.getByTestId("workflow-step-checkbox-WS-001")).toBeTruthy();
});
const toggle = screen.getByTestId("task-form-more-options-toggle");
expect(toggle).toHaveAttribute("aria-expanded", "false");
fireEvent.click(toggle);
await waitFor(() => {
expect(toggle).toHaveAttribute("aria-expanded", "true");
});
expect(screen.getByText(/Model Configuration/i)).toBeTruthy();
expect(screen.getByText(/Attachments/i)).toBeTruthy();
expect(screen.getByText(/Workflow Steps/i)).toBeTruthy();
});
it("sends undefined enabledWorkflowSteps when no defaultOn steps and user hasn't interacted", async () => { it("sends undefined enabledWorkflowSteps when no defaultOn steps and user hasn't interacted", async () => {
const { fetchWorkflowSteps } = await import("../../api"); const { fetchWorkflowSteps } = await import("../../api");
vi.mocked(fetchWorkflowSteps).mockResolvedValueOnce([ vi.mocked(fetchWorkflowSteps).mockResolvedValueOnce([

View File

@@ -1116,6 +1116,42 @@ describe("TaskForm defaultOn auto-selection (FN-883)", () => {
expect(onDefaultOnApplied).toHaveBeenCalledWith(["WS-001"]); expect(onDefaultOnApplied).toHaveBeenCalledWith(["WS-001"]);
}); });
it("auto-expands More options by default when advanced selections are prefilled", () => {
renderTaskForm({
mode: "create",
selectedWorkflowSteps: ["WS-001"],
});
expect(screen.getByTestId("task-form-more-options-toggle")).toHaveAttribute("aria-expanded", "true");
});
it("stays collapsed when auto-expand is disabled even with prefilled advanced selections", () => {
renderTaskForm({
mode: "create",
selectedWorkflowSteps: ["WS-001"],
autoExpandMoreOptionsOnSelection: false,
});
expect(screen.getByTestId("task-form-more-options-toggle")).toHaveAttribute("aria-expanded", "false");
});
it("hides advanced controls from interaction when More options is collapsed", () => {
renderTaskForm({ mode: "create" });
const toggle = screen.getByTestId("task-form-more-options-toggle");
const moreOptions = screen.getByTestId("task-form-more-options");
expect(toggle).toHaveAttribute("aria-expanded", "false");
expect(moreOptions).toHaveAttribute("hidden");
expect(moreOptions).toHaveAttribute("aria-hidden", "true");
fireEvent.click(toggle);
expect(toggle).toHaveAttribute("aria-expanded", "true");
expect(moreOptions).not.toHaveAttribute("hidden");
expect(moreOptions).toHaveAttribute("aria-hidden", "false");
});
it("does not auto-select workflow steps in edit mode", async () => { it("does not auto-select workflow steps in edit mode", async () => {
const { fetchWorkflowSteps } = await import("../../api"); const { fetchWorkflowSteps } = await import("../../api");
vi.mocked(fetchWorkflowSteps).mockResolvedValueOnce([ vi.mocked(fetchWorkflowSteps).mockResolvedValueOnce([