diff --git a/site/src/components/Select/Select.tsx b/site/src/components/Select/Select.tsx index 3d2f8ffc3b..f9261629e9 100644 --- a/site/src/components/Select/Select.tsx +++ b/site/src/components/Select/Select.tsx @@ -13,11 +13,13 @@ export const SelectGroup = SelectPrimitive.Group; export const SelectValue = SelectPrimitive.Value; +export type SelectTriggerProps = React.ComponentPropsWithoutRef< + typeof SelectPrimitive.Trigger +>; + export const SelectTrigger = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef & { - id?: string; - } + SelectTriggerProps >(({ className, children, id, ...props }, ref) => ( = { + title: "modules/tasks/TaskPrompt/PromptSelectTrigger", + component: PromptSelectTrigger, + args: { + children: "Select a version", + tooltip: "Template version", + }, + render: (args) => ( + + ), +}; + +export default meta; +type Story = StoryObj; + +export const Closed: Story = {}; + +export const Open: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const trigger = canvas.getByRole("combobox"); + await userEvent.click(trigger); + }, +}; diff --git a/site/src/modules/tasks/TaskPrompt/PromptSelectTrigger.tsx b/site/src/modules/tasks/TaskPrompt/PromptSelectTrigger.tsx new file mode 100644 index 0000000000..1a2a044a72 --- /dev/null +++ b/site/src/modules/tasks/TaskPrompt/PromptSelectTrigger.tsx @@ -0,0 +1,41 @@ +import { + SelectTrigger, + type SelectTriggerProps, +} from "components/Select/Select"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "components/Tooltip/Tooltip"; +import type { FC } from "react"; +import { cn } from "utils/cn"; + +type PromptSelectTriggerProps = SelectTriggerProps & { + tooltip: string; +}; + +export const PromptSelectTrigger: FC = ({ + className, + tooltip, + ...props +}) => { + return ( + + + + + + {tooltip} + + + ); +}; diff --git a/site/src/modules/tasks/TaskPrompt/TaskPrompt.tsx b/site/src/modules/tasks/TaskPrompt/TaskPrompt.tsx index 1d074010f0..706b3480c0 100644 --- a/site/src/modules/tasks/TaskPrompt/TaskPrompt.tsx +++ b/site/src/modules/tasks/TaskPrompt/TaskPrompt.tsx @@ -1,10 +1,6 @@ -import type { SelectTriggerProps } from "@radix-ui/react-select"; import { API } from "api/api"; import { getErrorDetail, getErrorMessage } from "api/errors"; -import { - templateVersionPresets, - templateVersions, -} from "api/queries/templates"; +import { templateVersionPresets } from "api/queries/templates"; import type { Preset, Task, @@ -20,7 +16,6 @@ import { Select, SelectContent, SelectItem, - SelectTrigger, SelectValue, } from "components/Select/Select"; import { Skeleton } from "components/Skeleton/Skeleton"; @@ -40,8 +35,9 @@ import { useMutation, useQuery, useQueryClient } from "react-query"; import TextareaAutosize, { type TextareaAutosizeProps, } from "react-textarea-autosize"; -import { cn } from "utils/cn"; import { docs } from "utils/docs"; +import { PromptSelectTrigger } from "./PromptSelectTrigger"; +import { TemplateVersionSelect } from "./TemplateVersionSelect"; type TaskPromptProps = { templates: Template[] | undefined; @@ -154,10 +150,6 @@ const CreateTaskForm: FC = ({ templates, onSuccess }) => { const [selectedVersionId, setSelectedVersionId] = useState( selectedTemplate.active_version_id, ); - const versionsQuery = useQuery({ - ...templateVersions(selectedTemplate.id), - enabled: permissions.updateTemplates, - }); useEffect(() => { setSelectedVersionId(selectedTemplate.active_version_id); }, [selectedTemplate]); @@ -280,7 +272,7 @@ const CreateTaskForm: FC = ({ templates, onSuccess }) => { defaultValue={templates[0].id} required > - + @@ -297,30 +289,17 @@ const CreateTaskForm: FC = ({ templates, onSuccess }) => { - {versionsQuery.data && ( + {permissions.updateTemplates && (
- + onValueChange={setSelectedVersionId} + />
)} @@ -340,7 +319,7 @@ const CreateTaskForm: FC = ({ templates, onSuccess }) => { value={selectedPresetId} onValueChange={setSelectedPresetId} > - + @@ -390,23 +369,6 @@ const CreateTaskForm: FC = ({ templates, onSuccess }) => { ); }; -const PromptSelectTrigger: FC = ({ - className, - ...props -}) => { - return ( - - ); -}; - type ExternalAuthButtonProps = { versionId: string; missedExternalAuth: TemplateVersionExternalAuth[]; diff --git a/site/src/modules/tasks/TaskPrompt/TemplateVersionSelect.stories.tsx b/site/src/modules/tasks/TaskPrompt/TemplateVersionSelect.stories.tsx new file mode 100644 index 0000000000..8c37edc301 --- /dev/null +++ b/site/src/modules/tasks/TaskPrompt/TemplateVersionSelect.stories.tsx @@ -0,0 +1,80 @@ +import { MockTemplate, MockTemplateVersion } from "testHelpers/entities"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { API } from "api/api"; +import { useState } from "react"; +import { spyOn, userEvent, within } from "storybook/test"; +import { daysAgo } from "utils/time"; +import { TemplateVersionSelect } from "./TemplateVersionSelect"; + +const meta: Meta = { + title: "modules/tasks/TaskPrompt/TemplateVersionSelect", + component: TemplateVersionSelect, + args: { + activeVersionId: MockTemplateVersion.id, + templateId: MockTemplate.id, + value: MockTemplateVersion.id, + }, + render: ({ value: defaultValue, ...args }) => { + const [value, setValue] = useState(defaultValue); + return ( + + ); + }, +}; + +export default meta; +type Story = StoryObj; + +const MockVersions = [ + { + ...MockTemplateVersion, + id: "v1.0.0", + name: "v1.0.0", + created_at: daysAgo(3), + }, + { + ...MockTemplateVersion, + id: "v2.0.0", + name: "v2.0.0", + created_at: daysAgo(2), + }, + { + ...MockTemplateVersion, + id: "v3.0.0", + name: "v3.0.0", + created_at: daysAgo(1), + }, +]; + +export const Loading: Story = { + beforeEach: () => { + spyOn(API, "getTemplateVersions").mockImplementation(() => { + return new Promise(() => {}); + }); + }, +}; + +export const Loaded: Story = { + args: { + activeVersionId: MockVersions[2].id, + value: MockVersions[2].id, + }, + beforeEach: () => { + spyOn(API, "getTemplateVersions").mockResolvedValue(MockVersions); + }, +}; + +export const Open: Story = { + args: { + activeVersionId: MockVersions[2].id, + value: MockVersions[2].id, + }, + beforeEach: () => { + spyOn(API, "getTemplateVersions").mockResolvedValue(MockVersions); + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const trigger = await canvas.findByRole("combobox"); + await userEvent.click(trigger); + }, +}; diff --git a/site/src/modules/tasks/TaskPrompt/TemplateVersionSelect.tsx b/site/src/modules/tasks/TaskPrompt/TemplateVersionSelect.tsx new file mode 100644 index 0000000000..8e81a5ba1d --- /dev/null +++ b/site/src/modules/tasks/TaskPrompt/TemplateVersionSelect.tsx @@ -0,0 +1,65 @@ +import { templateVersions } from "api/queries/templates"; +import { Badge } from "components/Badge/Badge"; +import { + Select, + SelectContent, + SelectItem, + SelectValue, +} from "components/Select/Select"; +import { Skeleton } from "components/Skeleton/Skeleton"; +import type { FC } from "react"; +import { useQuery } from "react-query"; +import { PromptSelectTrigger } from "./PromptSelectTrigger"; + +type TemplateVersionSelectProps = { + value: string; + templateId: string; + activeVersionId: string; + onValueChange: (value: string) => void; +}; + +export const TemplateVersionSelect: FC = ({ + templateId, + activeVersionId, + ...props +}) => { + const { data: versions } = useQuery({ + ...templateVersions(templateId), + select: (versions) => + versions + .filter((v) => !v.archived) + .toSorted((a, b) => { + return ( + new Date(b.created_at).getTime() - new Date(a.created_at).getTime() + ); + }), + }); + + if (!versions) { + return ; + } + + return ( + + ); +};