feat: select template version for tasks (#20146)

Allows users with `updateTemplates` permission to select a specific
template version when creating a task.

One of the biggest changes was moving `TaskPrompt` into `modules/task`
and adding a Storybook entry for it. I also moved some stories from
`TasksPage` to simplify its story.

<img width="1208" height="197" alt="Screenshot 2025-10-02 at 12 09 06"
src="https://github.com/user-attachments/assets/b85d2723-bb52-442b-b8eb-36721944a653"
/>

Closes https://github.com/coder/coder/issues/19986
This commit is contained in:
Bruno Quaresma
2025-10-06 10:23:53 -03:00
committed by GitHub
parent 3f49e28308
commit 840afb225b
4 changed files with 386 additions and 280 deletions
@@ -0,0 +1,296 @@
import {
MockAIPromptPresets,
MockNewTaskData,
MockPresets,
MockTask,
MockTasks,
MockTemplate,
MockTemplateVersion,
MockTemplateVersionExternalAuthGithub,
MockTemplateVersionExternalAuthGithubAuthenticated,
MockUserOwner,
mockApiError,
} from "testHelpers/entities";
import { withAuthProvider, withGlobalSnackbar } from "testHelpers/storybook";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { API } from "api/api";
import { expect, spyOn, userEvent, waitFor, within } from "storybook/test";
import type TasksPage from "../../../pages/TasksPage/TasksPage";
import { TaskPrompt } from "./TaskPrompt";
const meta: Meta<typeof TasksPage> = {
title: "modules/tasks/TaskPrompt",
component: TaskPrompt,
decorators: [withAuthProvider],
parameters: {
user: MockUserOwner,
permissions: {
updateTemplates: true,
},
},
beforeEach: () => {
spyOn(API, "getTemplateVersionExternalAuth").mockResolvedValue([]);
spyOn(API, "getTemplates").mockResolvedValue([
MockTemplate,
{
...MockTemplate,
id: "test-template-2",
name: "template 2",
display_name: "Template 2",
},
]);
spyOn(API, "getTemplateVersions").mockResolvedValue([
{
...MockTemplateVersion,
name: "v1.0.0",
},
]);
spyOn(API, "getTemplateVersionPresets").mockResolvedValue(null);
},
args: {
templates: [MockTemplate],
},
};
export default meta;
type Story = StoryObj<typeof TasksPage>;
export const LoadingTemplates: Story = {
args: {
templates: undefined,
},
};
export const EmptyTemplates: Story = {
args: {
templates: [],
},
};
export const WithPresets: Story = {
beforeEach: () => {
spyOn(API, "getTemplateVersionPresets").mockResolvedValue(MockPresets);
},
};
export const ReadOnlyPresetPrompt: Story = {
beforeEach: () => {
spyOn(API, "getTemplateVersionPresets").mockResolvedValue(
MockAIPromptPresets,
);
},
};
export const OnSuccess: Story = {
decorators: [withGlobalSnackbar],
parameters: {
permissions: {
updateTemplates: false,
},
},
beforeEach: () => {
const activeVersionId = `${MockTemplate.active_version_id}-latest`;
spyOn(API, "getTemplate").mockResolvedValue({
...MockTemplate,
active_version_id: activeVersionId,
});
spyOn(API.experimental, "createTask").mockResolvedValue(MockTask);
},
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
await step("Run task", async () => {
const prompt = await canvas.findByLabelText(/prompt/i);
await userEvent.type(prompt, MockNewTaskData.prompt);
const submitButton = canvas.getByRole("button", { name: /run task/i });
await waitFor(() => expect(submitButton).toBeEnabled());
await userEvent.click(submitButton);
});
await step("Uses latest template version", () => {
expect(API.experimental.createTask).toHaveBeenCalledWith(
MockUserOwner.id,
{
input: MockNewTaskData.prompt,
template_version_id: `${MockTemplate.active_version_id}-latest`,
template_version_preset_id: undefined,
},
);
});
await step("Displays success message", async () => {
const body = within(canvasElement.ownerDocument.body);
const successMessage = await body.findByText(/task created/i);
expect(successMessage).toBeInTheDocument();
});
},
};
export const SelectTemplateVersion: Story = {
decorators: [withGlobalSnackbar],
beforeEach: () => {
spyOn(API, "getTemplateVersions").mockResolvedValue([
{
...MockTemplateVersion,
id: "test-template-version-2",
name: "v2.0.0",
},
{
...MockTemplateVersion,
name: "v1.0.0",
},
]);
spyOn(API.experimental, "createTask").mockResolvedValue(MockTask);
},
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
await step("Fill prompt", async () => {
const prompt = await canvas.findByLabelText(/prompt/i);
await userEvent.type(prompt, MockNewTaskData.prompt);
});
await step("Select version", async () => {
const body = within(canvasElement.ownerDocument.body);
const versionSelect = await canvas.findByLabelText(/template version/i);
await userEvent.click(versionSelect);
const versionOption = await body.findByRole("option", {
name: /v2.0.0/i,
});
await userEvent.click(versionOption);
});
await step("Submit form", async () => {
const submitButton = canvas.getByRole("button", { name: /run task/i });
await waitFor(() => expect(submitButton).toBeEnabled());
await userEvent.click(submitButton);
});
await step("Uses selected version", () => {
expect(API.experimental.createTask).toHaveBeenCalledWith(
MockUserOwner.id,
{
input: MockNewTaskData.prompt,
template_version_id: "test-template-version-2",
template_version_preset_id: undefined,
},
);
});
await step("Displays success message", async () => {
const body = within(canvasElement.ownerDocument.body);
const successMessage = await body.findByText(/task created/i);
expect(successMessage).toBeInTheDocument();
});
},
};
export const OnError: Story = {
decorators: [withGlobalSnackbar],
beforeEach: () => {
spyOn(API, "getTemplates").mockResolvedValue([MockTemplate]);
spyOn(API, "getTemplate").mockResolvedValue(MockTemplate);
spyOn(API.experimental, "getTasks").mockResolvedValue(MockTasks);
spyOn(API.experimental, "createTask").mockRejectedValue(
mockApiError({
message: "Failed to create task",
detail: "You don't have permission to create tasks.",
}),
);
},
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
await step("Run task", async () => {
const prompt = await canvas.findByLabelText(/prompt/i);
await userEvent.type(prompt, "Create a new task");
const submitButton = canvas.getByRole("button", { name: /run task/i });
await waitFor(() => expect(submitButton).toBeEnabled());
await userEvent.click(submitButton);
});
await step("Verify error", async () => {
await canvas.findByText(/failed to create task/i);
});
},
};
export const AuthenticatedExternalAuth: Story = {
beforeEach: () => {
spyOn(API.experimental, "getTasks")
.mockResolvedValueOnce(MockTasks)
.mockResolvedValue([MockNewTaskData, ...MockTasks]);
spyOn(API.experimental, "createTask").mockResolvedValue(MockTask);
spyOn(API, "getTemplateVersionExternalAuth").mockResolvedValue([
MockTemplateVersionExternalAuthGithubAuthenticated,
]);
},
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
await step("Does not render external auth", async () => {
expect(
canvas.queryByText(/external authentication/),
).not.toBeInTheDocument();
});
},
parameters: {
chromatic: {
disableSnapshot: true,
},
},
};
export const MissingExternalAuth: Story = {
beforeEach: () => {
spyOn(API.experimental, "getTasks")
.mockResolvedValueOnce(MockTasks)
.mockResolvedValue([MockNewTaskData, ...MockTasks]);
spyOn(API.experimental, "createTask").mockResolvedValue(MockTask);
spyOn(API, "getTemplateVersionExternalAuth").mockResolvedValue([
MockTemplateVersionExternalAuthGithub,
]);
},
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
await step("Submit is disabled", async () => {
const prompt = await canvas.findByLabelText(/prompt/i);
await userEvent.type(prompt, MockNewTaskData.prompt);
const submitButton = canvas.getByRole("button", { name: /run task/i });
expect(submitButton).toBeDisabled();
});
await step("Renders external authentication", async () => {
await canvas.findByRole("button", { name: /connect to github/i });
});
},
};
export const ExternalAuthError: Story = {
beforeEach: () => {
spyOn(API.experimental, "getTasks")
.mockResolvedValueOnce(MockTasks)
.mockResolvedValue([MockNewTaskData, ...MockTasks]);
spyOn(API.experimental, "createTask").mockResolvedValue(MockTask);
spyOn(API, "getTemplateVersionExternalAuth").mockRejectedValue(
mockApiError({
message: "Failed to load external auth",
}),
);
},
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
await step("Submit is disabled", async () => {
const prompt = await canvas.findByLabelText(/prompt/i);
await userEvent.type(prompt, MockNewTaskData.prompt);
const submitButton = canvas.getByRole("button", { name: /run task/i });
expect(submitButton).toBeDisabled();
});
await step("Renders error", async () => {
await canvas.findByText(/failed to load external auth/i);
});
},
};
@@ -1,7 +1,10 @@
import type { SelectTriggerProps } from "@radix-ui/react-select";
import { API } from "api/api";
import { getErrorDetail, getErrorMessage } from "api/errors";
import { templateVersionPresets } from "api/queries/templates";
import {
templateVersionPresets,
templateVersions,
} from "api/queries/templates";
import type {
Preset,
Task,
@@ -135,12 +138,14 @@ type CreateTaskFormProps = {
};
const CreateTaskForm: FC<CreateTaskFormProps> = ({ templates, onSuccess }) => {
const { user } = useAuthenticated();
const { user, permissions } = useAuthenticated();
const queryClient = useQueryClient();
const [prompt, setPrompt] = useState("");
// Template
const [selectedTemplateId, setSelectedTemplateId] = useState<string>(
templates[0].id,
);
const [selectedPresetId, setSelectedPresetId] = useState<string>();
const selectedTemplate = templates.find(
(t) => t.id === selectedTemplateId,
) as Template;
@@ -152,24 +157,38 @@ const CreateTaskForm: FC<CreateTaskFormProps> = ({ templates, onSuccess }) => {
isLoadingExternalAuth,
} = useExternalAuth(selectedTemplate.active_version_id);
// Fetch presets when template changes
const { data: presets, isLoading: isLoadingPresets } = useQuery(
templateVersionPresets(selectedTemplate.active_version_id),
// Template versions
const [selectedVersionId, setSelectedVersionId] = useState(
selectedTemplate.active_version_id,
);
const defaultPreset = presets?.find((p) => p.Default);
const versionsQuery = useQuery({
...templateVersions(selectedTemplate.id),
enabled: permissions.updateTemplates,
});
// Handle preset selection when data changes
// Presets
const { data: presets, isLoading: isLoadingPresets } = useQuery(
templateVersionPresets(selectedVersionId),
);
const [selectedPresetId, setSelectedPresetId] = useState<string>();
useEffect(() => {
setSelectedPresetId(defaultPreset?.ID);
}, [defaultPreset?.ID]);
// Extract AI prompt from selected preset
const defaultPreset = presets?.find((p) => p.Default);
setSelectedPresetId(defaultPreset?.ID ?? presets?.[0]?.ID);
}, [presets]);
const selectedPreset = presets?.find((p) => p.ID === selectedPresetId);
const presetAIPrompt = selectedPreset?.Parameters?.find(
// Read-only prompt if defined in preset
const presetPrompt = selectedPreset?.Parameters?.find(
(param) => param.Name === AI_PROMPT_PARAMETER_NAME,
)?.Value;
const isPromptReadOnly = !!presetAIPrompt;
const isPromptReadOnly = !!presetPrompt;
useEffect(() => {
if (presetPrompt) {
setPrompt(presetPrompt);
}
}, [presetPrompt]);
// External Auth
const missedExternalAuth = externalAuth?.filter(
(auth) => !auth.optional && !auth.authenticated,
);
@@ -178,13 +197,26 @@ const CreateTaskForm: FC<CreateTaskFormProps> = ({ templates, onSuccess }) => {
: true;
const createTaskMutation = useMutation({
mutationFn: async ({ prompt }: CreateTaskMutationFnProps) =>
createTaskWithLatestTemplateVersion(
mutationFn: async ({ prompt }: CreateTaskMutationFnProps) => {
// Users with updateTemplates permission can select the version to use.
if (permissions.updateTemplates) {
return API.experimental.createTask(user.id, {
input: prompt,
template_version_id: selectedVersionId,
template_version_preset_id: selectedPresetId,
});
}
// For regular users we want to enforce task creation to always use the latest
// active template version, to avoid issues when the active version changes
// between template load and user action.
return createTaskWithLatestTemplateVersion(
prompt,
user.id,
selectedTemplate.id,
selectedPresetId,
),
);
},
onSuccess: async (task) => {
await queryClient.invalidateQueries({ queryKey: ["tasks"] });
onSuccess(task);
@@ -194,10 +226,6 @@ const CreateTaskForm: FC<CreateTaskFormProps> = ({ templates, onSuccess }) => {
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const form = e.currentTarget;
const formData = new FormData(form);
const prompt = presetAIPrompt || (formData.get("prompt") as string);
try {
await createTaskMutation.mutateAsync({
prompt,
@@ -225,7 +253,7 @@ const CreateTaskForm: FC<CreateTaskFormProps> = ({ templates, onSuccess }) => {
htmlFor="prompt"
className={
isPromptReadOnly
? "text-xs font-medium text-content-primary mb-2 block"
? "text-xs font-medium text-content-primary block px-3 pt-2"
: "sr-only"
}
>
@@ -233,12 +261,13 @@ const CreateTaskForm: FC<CreateTaskFormProps> = ({ templates, onSuccess }) => {
</label>
<PromptTextarea
required
value={presetAIPrompt || undefined}
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
readOnly={isPromptReadOnly}
/>
<div className="flex items-center justify-between pt-2">
<div className="flex items-center gap-1">
<div className="flex flex-col gap-1">
<div>
<label htmlFor="templateID" className="sr-only">
Template
</label>
@@ -265,7 +294,34 @@ const CreateTaskForm: FC<CreateTaskFormProps> = ({ templates, onSuccess }) => {
</Select>
</div>
<div className="flex flex-col gap-1">
{versionsQuery.data && (
<div>
<label htmlFor="versionId" className="sr-only">
Template version
</label>
<Select
name="versionId"
onValueChange={(value) => setSelectedVersionId(value)}
value={selectedVersionId}
required
>
<PromptSelectTrigger id="versionId">
<SelectValue placeholder="Select a version" />
</PromptSelectTrigger>
<SelectContent>
{versionsQuery.data.map((version) => {
return (
<SelectItem value={version.id} key={version.id}>
{version.name}
</SelectItem>
);
})}
</SelectContent>
</Select>
</div>
)}
<div>
<label htmlFor="presetID" className="sr-only">
Preset
</label>
@@ -273,11 +329,12 @@ const CreateTaskForm: FC<CreateTaskFormProps> = ({ templates, onSuccess }) => {
<Skeleton className="w-[320px] h-8" />
) : (
presets &&
presets.length > 0 && (
presets.length > 0 &&
selectedPresetId && (
<Select
key={`preset-select-${selectedTemplate.active_version_id}`}
name="presetID"
value={selectedPresetId || undefined}
value={selectedPresetId}
onValueChange={setSelectedPresetId}
>
<PromptSelectTrigger id="presetID">
@@ -438,6 +495,7 @@ async function createTaskWithLatestTemplateVersion(
const PromptTextarea: FC<TextareaAutosizeProps> = (props) => {
return (
<TextareaAutosize
{...props}
required
id="prompt"
name="prompt"
+4 -252
View File
@@ -1,25 +1,14 @@
import {
MockAIPromptPresets,
MockNewTaskData,
MockPresets,
MockTask,
MockTasks,
MockTemplate,
MockTemplateVersionExternalAuthGithub,
MockTemplateVersionExternalAuthGithubAuthenticated,
MockUserOwner,
mockApiError,
} from "testHelpers/entities";
import {
withAuthProvider,
withGlobalSnackbar,
withProxyProvider,
} from "testHelpers/storybook";
import { withAuthProvider, withProxyProvider } from "testHelpers/storybook";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { API } from "api/api";
import { MockUsers } from "pages/UsersPage/storybookData/users";
import { expect, spyOn, userEvent, waitFor, within } from "storybook/test";
import { reactRouterParameters } from "storybook-addon-remix-react-router";
import { expect, spyOn, userEvent, within } from "storybook/test";
import TasksPage from "./TasksPage";
const meta: Meta<typeof TasksPage> = {
@@ -54,7 +43,7 @@ const meta: Meta<typeof TasksPage> = {
export default meta;
type Story = StoryObj<typeof TasksPage>;
export const LoadingAITemplates: Story = {
export const LoadingTemplates: Story = {
beforeEach: () => {
spyOn(API, "getTemplates").mockImplementation(
() => new Promise(() => 1000 * 60 * 60),
@@ -62,7 +51,7 @@ export const LoadingAITemplates: Story = {
},
};
export const LoadingAITemplatesError: Story = {
export const LoadingTemplatesError: Story = {
beforeEach: () => {
spyOn(API, "getTemplates").mockRejectedValue(
mockApiError({
@@ -73,13 +62,6 @@ export const LoadingAITemplatesError: Story = {
},
};
export const EmptyAITemplates: Story = {
beforeEach: () => {
spyOn(API, "getTemplates").mockResolvedValue([]);
spyOn(API.experimental, "getTasks").mockResolvedValue([]);
},
};
export const LoadingTasks: Story = {
beforeEach: () => {
spyOn(API, "getTemplates").mockResolvedValue([MockTemplate]);
@@ -123,58 +105,6 @@ export const LoadedTasks: Story = {
},
};
export const LoadedTasksWithPresets: Story = {
beforeEach: () => {
const mockTemplateWithPresets = {
...MockTemplate,
id: "test-template-2",
name: "template-with-presets",
display_name: "Template with Presets",
};
spyOn(API, "getTemplates").mockResolvedValue([
MockTemplate,
mockTemplateWithPresets,
]);
spyOn(API.experimental, "getTasks").mockResolvedValue(MockTasks);
spyOn(API, "getTemplateVersionPresets").mockImplementation(
async (versionId) => {
// Return presets only for the second template
if (versionId === mockTemplateWithPresets.active_version_id) {
return MockPresets;
}
return null;
},
);
},
};
export const LoadedTasksWithAIPromptPresets: Story = {
beforeEach: () => {
const mockTemplateWithPresets = {
...MockTemplate,
id: "test-template-2",
name: "template-with-presets",
display_name: "Template with AI Prompt Presets",
};
spyOn(API, "getTemplates").mockResolvedValue([
MockTemplate,
mockTemplateWithPresets,
]);
spyOn(API.experimental, "getTasks").mockResolvedValue(MockTasks);
spyOn(API, "getTemplateVersionPresets").mockImplementation(
async (versionId) => {
// Return presets only for the second template
if (versionId === mockTemplateWithPresets.active_version_id) {
return MockAIPromptPresets;
}
return null;
},
);
},
};
export const LoadedTasksWaitingForInput: Story = {
beforeEach: () => {
const [firstTask, ...otherTasks] = MockTasks;
@@ -225,184 +155,6 @@ export const LoadedTasksWaitingForInputTab: Story = {
},
};
export const CreateTaskSuccessfully: Story = {
decorators: [withGlobalSnackbar],
parameters: {
reactRouter: reactRouterParameters({
location: {
path: "/tasks",
},
routing: [
{
path: "/tasks",
useStoryElement: true,
},
{
path: "/tasks/:ownerName/:workspaceName",
element: <h1>Task page</h1>,
},
],
}),
},
beforeEach: () => {
const activeVersionId = `${MockTemplate.active_version_id}-latest`;
spyOn(API, "getTemplates").mockResolvedValue([MockTemplate]);
spyOn(API, "getTemplate").mockResolvedValue({
...MockTemplate,
active_version_id: activeVersionId,
});
spyOn(API.experimental, "getTasks")
.mockResolvedValueOnce(MockTasks)
.mockResolvedValue([MockNewTaskData, ...MockTasks]);
spyOn(API.experimental, "createTask").mockResolvedValue(MockTask);
},
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
await step("Run task", async () => {
const prompt = await canvas.findByLabelText(/prompt/i);
await userEvent.type(prompt, MockNewTaskData.prompt);
const submitButton = canvas.getByRole("button", { name: /run task/i });
await waitFor(() => expect(submitButton).toBeEnabled());
await userEvent.click(submitButton);
});
await step("Uses latest template version", () => {
expect(API.experimental.createTask).toHaveBeenCalledWith(
MockUserOwner.id,
{
input: MockNewTaskData.prompt,
template_version_id: `${MockTemplate.active_version_id}-latest`,
template_version_preset_id: undefined,
},
);
});
await step("Displays success message", async () => {
const body = within(canvasElement.ownerDocument.body);
const successMessage = await body.findByText(/task created/i);
expect(successMessage).toBeInTheDocument();
});
await step("Find task in the table", async () => {
const table = canvasElement.querySelector("table");
await waitFor(() => {
expect(table).toHaveTextContent(MockNewTaskData.prompt);
});
});
},
};
export const CreateTaskError: Story = {
decorators: [withGlobalSnackbar],
beforeEach: () => {
spyOn(API, "getTemplates").mockResolvedValue([MockTemplate]);
spyOn(API, "getTemplate").mockResolvedValue(MockTemplate);
spyOn(API.experimental, "getTasks").mockResolvedValue(MockTasks);
spyOn(API.experimental, "createTask").mockRejectedValue(
mockApiError({
message: "Failed to create task",
detail: "You don't have permission to create tasks.",
}),
);
},
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
await step("Run task", async () => {
const prompt = await canvas.findByLabelText(/prompt/i);
await userEvent.type(prompt, "Create a new task");
const submitButton = canvas.getByRole("button", { name: /run task/i });
await waitFor(() => expect(submitButton).toBeEnabled());
await userEvent.click(submitButton);
});
await step("Verify error", async () => {
await canvas.findByText(/failed to create task/i);
});
},
};
export const WithAuthenticatedExternalAuth: Story = {
beforeEach: () => {
spyOn(API.experimental, "getTasks")
.mockResolvedValueOnce(MockTasks)
.mockResolvedValue([MockNewTaskData, ...MockTasks]);
spyOn(API.experimental, "createTask").mockResolvedValue(MockTask);
spyOn(API, "getTemplateVersionExternalAuth").mockResolvedValue([
MockTemplateVersionExternalAuthGithubAuthenticated,
]);
},
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
await step("Does not render external auth", async () => {
expect(
canvas.queryByText(/external authentication/),
).not.toBeInTheDocument();
});
},
parameters: {
chromatic: {
disableSnapshot: true,
},
},
};
export const MissingExternalAuth: Story = {
beforeEach: () => {
spyOn(API.experimental, "getTasks")
.mockResolvedValueOnce(MockTasks)
.mockResolvedValue([MockNewTaskData, ...MockTasks]);
spyOn(API.experimental, "createTask").mockResolvedValue(MockTask);
spyOn(API, "getTemplateVersionExternalAuth").mockResolvedValue([
MockTemplateVersionExternalAuthGithub,
]);
},
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
await step("Submit is disabled", async () => {
const prompt = await canvas.findByLabelText(/prompt/i);
await userEvent.type(prompt, MockNewTaskData.prompt);
const submitButton = canvas.getByRole("button", { name: /run task/i });
expect(submitButton).toBeDisabled();
});
await step("Renders external authentication", async () => {
await canvas.findByRole("button", { name: /connect to github/i });
});
},
};
export const ExternalAuthError: Story = {
beforeEach: () => {
spyOn(API.experimental, "getTasks")
.mockResolvedValueOnce(MockTasks)
.mockResolvedValue([MockNewTaskData, ...MockTasks]);
spyOn(API.experimental, "createTask").mockResolvedValue(MockTask);
spyOn(API, "getTemplateVersionExternalAuth").mockRejectedValue(
mockApiError({
message: "Failed to load external auth",
}),
);
},
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
await step("Submit is disabled", async () => {
const prompt = await canvas.findByLabelText(/prompt/i);
await userEvent.type(prompt, MockNewTaskData.prompt);
const submitButton = canvas.getByRole("button", { name: /run task/i });
expect(submitButton).toBeDisabled();
});
await step("Renders error", async () => {
await canvas.findByText(/failed to load external auth/i);
});
},
};
export const NonAdmin: Story = {
parameters: {
permissions: {
+1 -1
View File
@@ -11,11 +11,11 @@ import {
} from "components/PageHeader/PageHeader";
import { useAuthenticated } from "hooks";
import { useSearchParamsKey } from "hooks/useSearchParamsKey";
import { TaskPrompt } from "modules/tasks/TaskPrompt/TaskPrompt";
import type { FC } from "react";
import { useQuery } from "react-query";
import { cn } from "utils/cn";
import { pageTitle } from "utils/page";
import { TaskPrompt } from "./TaskPrompt";
import { TasksTable } from "./TasksTable";
import { UsersCombobox } from "./UsersCombobox";