Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,30 @@ import { CreateFilesetStart } from '@studio/components/CreateFilesetStart';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

const renderStart = () => {
const onContinue = vi.fn();
render(<CreateFilesetStart onContinue={onContinue} />);
return { onContinue };
};

describe('CreateFilesetStart', () => {
it('renders all four start options', () => {
render(<CreateFilesetStart onContinue={vi.fn()} />);
it('renders all start options', () => {
renderStart();

expect(screen.getByText('Describe with AI')).toBeInTheDocument();
expect(screen.getByText('Start from a template')).toBeInTheDocument();
expect(screen.getByText('Build from scratch')).toBeInTheDocument();
});

it('shows no Continue footer until a selectable option is chosen', () => {
render(<CreateFilesetStart onContinue={vi.fn()} />);
it('shows no Continue footer until an option is chosen', () => {
renderStart();

expect(screen.queryByRole('button', { name: /continue/i })).not.toBeInTheDocument();
});

it('does not select disabled options (they are no-ops)', async () => {
const user = userEvent.setup();
const onContinue = vi.fn();
render(<CreateFilesetStart onContinue={onContinue} />);
const { onContinue } = renderStart();

await user.click(screen.getByText('Describe with AI'));

Expand All @@ -33,56 +38,57 @@ describe('CreateFilesetStart', () => {

it('selecting Build from scratch reveals Continue and invokes onContinue with "scratch"', async () => {
const user = userEvent.setup();
const onContinue = vi.fn();
render(<CreateFilesetStart onContinue={onContinue} />);
const { onContinue } = renderStart();

await user.click(screen.getByText('Build from scratch'));

const continueButton = screen.getByRole('button', { name: /continue/i });
expect(continueButton).toBeInTheDocument();
expect(continueButton).toBeEnabled();

await user.click(continueButton);
expect(onContinue).toHaveBeenCalledTimes(1);
expect(onContinue).toHaveBeenCalledWith('scratch');
expect(onContinue).toHaveBeenCalledWith({ optionId: 'scratch' });
});

it('reveals template cards but no Continue until a template is chosen', async () => {
it('reveals template cards but keeps Continue disabled until a template is chosen', async () => {
const user = userEvent.setup();
render(<CreateFilesetStart onContinue={vi.fn()} />);
renderStart();

await user.click(screen.getByText('Start from a template'));

expect(screen.getByText('Instruction fine-tuning (SFT)')).toBeInTheDocument();
expect(screen.queryByRole('button', { name: /continue/i })).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: /continue/i })).toBeDisabled();
expect(screen.getByText('Pick a recipe to continue.')).toBeInTheDocument();
});

it('choosing a template reveals Continue and invokes onContinue with the template id', async () => {
it('choosing a template enables Continue and invokes onContinue with the template id', async () => {
const user = userEvent.setup();
const onContinue = vi.fn();
render(<CreateFilesetStart onContinue={onContinue} />);
const { onContinue } = renderStart();

await user.click(screen.getByText('Start from a template'));
await user.click(screen.getByText('Instruction fine-tuning (SFT)'));

const continueButton = screen.getByRole('button', { name: /continue/i });
await user.click(continueButton);
await user.click(screen.getByRole('button', { name: /continue/i }));

expect(onContinue).toHaveBeenCalledTimes(1);
expect(onContinue).toHaveBeenCalledWith('template', 'sft-instruction');
expect(onContinue).toHaveBeenCalledWith({
optionId: 'template',
templateId: 'sft-instruction',
});
});

it('switching options clears a prior template selection', async () => {
const user = userEvent.setup();
render(<CreateFilesetStart onContinue={vi.fn()} />);
renderStart();

await user.click(screen.getByText('Start from a template'));
await user.click(screen.getByText('Instruction fine-tuning (SFT)'));
expect(screen.getByRole('button', { name: /continue/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /continue/i })).toBeEnabled();

await user.click(screen.getByText('Build from scratch'));
await user.click(screen.getByText('Start from a template'));

// Template selection was reset when the option changed, so Continue is gone again.
expect(screen.queryByRole('button', { name: /continue/i })).not.toBeInTheDocument();
// Template selection was reset when the option changed, so Continue is blocked again.
expect(screen.getByRole('button', { name: /continue/i })).toBeDisabled();
});
});
20 changes: 15 additions & 5 deletions web/packages/studio/src/components/CreateFilesetStart/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ import type {
import { ArrowRight } from 'lucide-react';
import { useState, type FC } from 'react';

/** Why Continue is unavailable, shown next to the disabled button. */
const BLOCKED_HINT: Partial<Record<StartOptionId, string>> = {
template: 'Pick a recipe to continue.',
};

export const CreateFilesetStart: FC<CreateFilesetStartProps> = ({ onContinue }) => {
const [selectedId, setSelectedId] = useState<StartOptionId | null>(null);
const [selectedTemplateId, setSelectedTemplateId] = useState<string | null>(null);
Expand All @@ -38,9 +43,9 @@ export const CreateFilesetStart: FC<CreateFilesetStartProps> = ({ onContinue })
const handleContinue = () => {
if (!selectedOption) return;
if (selectedOption.id === 'template' && selectedTemplateId) {
onContinue(selectedOption.id, selectedTemplateId);
} else {
onContinue(selectedOption.id);
onContinue({ optionId: 'template', templateId: selectedTemplateId });
} else if (selectedOption.id === 'scratch') {
onContinue({ optionId: 'scratch' });
}
};

Expand Down Expand Up @@ -80,13 +85,18 @@ export const CreateFilesetStart: FC<CreateFilesetStartProps> = ({ onContinue })
</Stack>
</Block>

{canContinue ? (
{selectedOption ? (
<Flex
align="center"
justify="end"
className="shrink-0 gap-4 border-t border-base bg-surface-base px-10 py-4"
>
<Button color="brand" kind="primary" onClick={handleContinue}>
{!canContinue && BLOCKED_HINT[selectedOption.id] ? (
<Text kind="body/regular/sm" className="text-secondary">
{BLOCKED_HINT[selectedOption.id]}
</Text>
) : null}
<Button color="brand" kind="primary" onClick={handleContinue} disabled={!canContinue}>
Continue
<ArrowRight size={16} aria-hidden />
</Button>
Expand Down
10 changes: 5 additions & 5 deletions web/packages/studio/src/components/CreateFilesetStart/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,10 @@ export interface StartOptionDetailProps {
onSelectTemplate: (templateId: string) => void;
}

/** What the user confirmed via the Continue footer, carrying that option's payload. */
export type StartSelection = { optionId: 'scratch' } | { optionId: 'template'; templateId: string };

export interface CreateFilesetStartProps {
/**
* Fired when the user confirms a selected start option via the Continue footer. For
* the "template" option, the chosen template id is passed as the second argument.
*/
onContinue: (optionId: StartOptionId, templateId?: string) => void;
/** Fired when the user confirms a selected start option via the Continue footer. */
onContinue: (selection: StartSelection) => void;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import { AccessibleTitle } from '@studio/components/AccessibleTitle';
import { CreateFilesetStart } from '@studio/components/CreateFilesetStart';
import type { StartOptionId } from '@studio/components/CreateFilesetStart/types';
import type { StartSelection } from '@studio/components/CreateFilesetStart/types';
import { useWorkspaceFromPath } from '@studio/hooks/useWorkspaceFromPath';
import { useBreadcrumbs } from '@studio/providers/breadcrumbs/useBreadcrumbs';
import { getDataDesignerJobBuildRoute, getDataDesignerJobListRoute } from '@studio/routes/utils';
Expand All @@ -21,11 +21,14 @@ export const NewDataDesignerJobRoute: FC = () => {
],
});

const handleContinue = (optionId: StartOptionId, templateId?: string) => {
if (optionId === 'scratch') {
navigate(getDataDesignerJobBuildRoute(workspace));
} else if (optionId === 'template' && templateId) {
navigate(`${getDataDesignerJobBuildRoute(workspace)}?template=${templateId}`);
const handleContinue = (selection: StartSelection) => {
switch (selection.optionId) {
case 'scratch':
navigate(getDataDesignerJobBuildRoute(workspace));
break;
case 'template':
navigate(`${getDataDesignerJobBuildRoute(workspace)}?template=${selection.templateId}`);
break;
}
};

Expand Down
Loading