From 6c8a8647f69920448f34052ad19c2c34293c216f Mon Sep 17 00:00:00 2001 From: Jake Howell Date: Thu, 6 Aug 2026 14:41:46 +1000 Subject: [PATCH] fix(site): name template versions in promote/archive dialogs and toasts (#27633) > [!NOTE] > These were previously returning as typed values in the `api.ts`, however, they were not actually typed in this way and updating them wouldn't have worked for the `promotion` as it would fall back to needing to validate against the template. Archive toasts showed `"undefined"` because the API does not return a `TemplateVersion`. Toast/dialog copy now comes from the selected version. - Hold the full `TemplateVersion` for promote/archive confirms (global pattern elsewhere) - Name the version in confirm dialogs and success/error toasts - Type archive/unarchive API helpers as `Promise` image --- site/src/api/api.ts | 13 +- .../TemplateVersionsPage.tsx | 157 ++++++++---------- .../TemplateVersionsPage/VersionRow.tsx | 8 +- .../TemplateVersionsPage/VersionsTable.tsx | 12 +- 4 files changed, 89 insertions(+), 101 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 1d381a564d..484fbb7aa4 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1166,19 +1166,18 @@ class ApiMethods { return response.data; }; - archiveTemplateVersion = async (templateVersionId: string) => { - const response = await this.axios.post( + archiveTemplateVersion = async (templateVersionId: string): Promise => { + await this.axios.post( `/api/v2/templateversions/${templateVersionId}/archive`, ); - - return response.data; }; - unarchiveTemplateVersion = async (templateVersionId: string) => { - const response = await this.axios.post( + unarchiveTemplateVersion = async ( + templateVersionId: string, + ): Promise => { + await this.axios.post( `/api/v2/templateversions/${templateVersionId}/unarchive`, ); - return response.data; }; /** diff --git a/site/src/pages/TemplatePage/TemplateVersionsPage/TemplateVersionsPage.tsx b/site/src/pages/TemplatePage/TemplateVersionsPage/TemplateVersionsPage.tsx index 36ad4df2ba..592f5745d2 100644 --- a/site/src/pages/TemplatePage/TemplateVersionsPage/TemplateVersionsPage.tsx +++ b/site/src/pages/TemplatePage/TemplateVersionsPage/TemplateVersionsPage.tsx @@ -8,6 +8,7 @@ import { templateVersions, templateVersionsQueryKey, } from "#/api/queries/templates"; +import type { TemplateVersion } from "#/api/typesGenerated"; import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog"; import { linkToTemplate, useLinks } from "#/modules/navigation"; import { useTemplateLayoutContext } from "#/pages/TemplatePage/TemplateLayout"; @@ -27,84 +28,27 @@ const TemplateVersionsPage = () => { const [latestActiveVersion, setLatestActiveVersion] = useState( template.active_version_id, ); - const { mutate: promoteVersion, isPending: isPromoting } = useMutation({ + const [versionToPromote, setVersionToPromote] = useState< + TemplateVersion | undefined + >(); + const [versionToArchive, setVersionToArchive] = useState< + TemplateVersion | undefined + >(); + + const { mutateAsync: promoteVersion, isPending: isPromoting } = useMutation({ mutationFn: (templateVersionId: string) => { return API.updateActiveTemplateVersion(template.id, { id: templateVersionId, }); }, - onSuccess: async () => { - const versionName = data?.find( - (v) => v.id === selectedVersionIdToPromote, - )?.name; - setLatestActiveVersion(selectedVersionIdToPromote as string); - setSelectedVersionIdToPromote(undefined); - toast.success( - versionName - ? `Version "${versionName}" promoted successfully.` - : "Version promoted successfully.", - { - action: { - label: "View template", - onClick: () => navigate(templateLink), - }, - }, - ); - }, - onError: (error) => { - const versionName = data?.find( - (v) => v.id === selectedVersionIdToPromote, - )?.name; - toast.error( - getErrorMessage( - error, - versionName - ? `Failed to promote version "${versionName}".` - : "Failed to promote version.", - ), - { - description: getErrorDetail(error), - }, - ); - }, }); - const { mutate: archiveVersion, isPending: isArchiving } = useMutation({ + const { mutateAsync: archiveVersion, isPending: isArchiving } = useMutation({ mutationFn: (templateVersionId: string) => { return API.archiveTemplateVersion(templateVersionId); }, - onSuccess: async (data) => { - await queryClient.invalidateQueries({ - queryKey: templateVersionsQueryKey(template.id), - }); - setSelectedVersionIdToArchive(undefined); - toast.success(`Version "${data.name}" archived successfully.`); - }, - onError: (error) => { - const versionName = data?.find( - (v) => v.id === selectedVersionIdToArchive, - )?.name; - toast.error( - getErrorMessage( - error, - versionName - ? `Failed to archive version "${versionName}".` - : "Failed to archive version.", - ), - { - description: getErrorDetail(error), - }, - ); - }, }); - const [selectedVersionIdToPromote, setSelectedVersionIdToPromote] = useState< - string | undefined - >(); - const [selectedVersionIdToArchive, setSelectedVersionIdToArchive] = useState< - string | undefined - >(); - return ( <> {getTemplatePageTitle("Versions", template)} @@ -112,44 +56,89 @@ const TemplateVersionsPage = () => { - {/* Promote confirm */} { - promoteVersion(selectedVersionIdToPromote as string); + open={Boolean(versionToPromote)} + onConfirm={async () => { + if (!versionToPromote) { + return; + } + const { id, name } = versionToPromote; + try { + await promoteVersion(id); + setLatestActiveVersion(id); + setVersionToPromote(undefined); + toast.success(`Version "${name}" promoted successfully.`, { + action: { + label: "View template", + onClick: () => navigate(templateLink), + }, + }); + } catch (error) { + toast.error( + getErrorMessage(error, `Failed to promote version "${name}".`), + { + description: getErrorDetail(error), + }, + ); + } }} - onClose={() => setSelectedVersionIdToPromote(undefined)} + onClose={() => setVersionToPromote(undefined)} title="Promote version" confirmLoading={isPromoting} confirmText="Promote" - description="Are you sure you want to promote this version? Workspaces will be prompted to “Update” to this version once promoted." + description={ + <> + Are you sure you want to promote version{" "} + {versionToPromote?.name}? Workspaces will be + prompted to “Update” to this version once promoted. + + } /> - {/* Archive Confirm */} { - archiveVersion(selectedVersionIdToArchive as string); + open={Boolean(versionToArchive)} + onConfirm={async () => { + if (!versionToArchive) { + return; + } + const { id, name } = versionToArchive; + try { + await archiveVersion(id); + await queryClient.invalidateQueries({ + queryKey: templateVersionsQueryKey(template.id), + }); + setVersionToArchive(undefined); + toast.success(`Version "${name}" archived successfully.`); + } catch (error) { + toast.error( + getErrorMessage(error, `Failed to archive version "${name}".`), + { + description: getErrorDetail(error), + }, + ); + } }} - onClose={() => setSelectedVersionIdToArchive(undefined)} + onClose={() => setVersionToArchive(undefined)} title="Archive version" confirmLoading={isArchiving} confirmText="Archive" - description="Are you sure you want to archive this version (this is reversible)? Archived versions cannot be used by workspaces." + description={ + <> + Are you sure you want to archive version{" "} + {versionToArchive?.name}? This is reversible. + Archived versions cannot be used by workspaces. + + } /> ); diff --git a/site/src/pages/TemplatePage/TemplateVersionsPage/VersionRow.tsx b/site/src/pages/TemplatePage/TemplateVersionsPage/VersionRow.tsx index e46716427b..9a51b9334e 100644 --- a/site/src/pages/TemplatePage/TemplateVersionsPage/VersionRow.tsx +++ b/site/src/pages/TemplatePage/TemplateVersionsPage/VersionRow.tsx @@ -14,8 +14,8 @@ interface VersionRowProps { version: TemplateVersion; isActive: boolean; isLatest: boolean; - onPromoteClick?: (templateVersionId: string) => void; - onArchiveClick?: (templateVersionId: string) => void; + onPromoteClick?: (version: TemplateVersion) => void; + onArchiveClick?: (version: TemplateVersion) => void; } export const VersionRow: FC = ({ @@ -91,7 +91,7 @@ export const VersionRow: FC = ({ onClick={(e) => { e.preventDefault(); e.stopPropagation(); - onArchiveClick?.(version.id); + onArchiveClick?.(version); }} > Archive… @@ -105,7 +105,7 @@ export const VersionRow: FC = ({ onClick={(e) => { e.preventDefault(); e.stopPropagation(); - onPromoteClick?.(version.id); + onPromoteClick?.(version); }} > Promote… diff --git a/site/src/pages/TemplatePage/TemplateVersionsPage/VersionsTable.tsx b/site/src/pages/TemplatePage/TemplateVersionsPage/VersionsTable.tsx index 5bddffb197..628a1e4ae5 100644 --- a/site/src/pages/TemplatePage/TemplateVersionsPage/VersionsTable.tsx +++ b/site/src/pages/TemplatePage/TemplateVersionsPage/VersionsTable.tsx @@ -1,5 +1,5 @@ import type { FC } from "react"; -import type * as TypesGen from "#/api/typesGenerated"; +import type { TemplateVersion } from "#/api/typesGenerated"; import { Table, TableBody } from "#/components/Table/Table"; import { TableEmpty } from "#/components/TableEmpty/TableEmpty"; import { TableLoader } from "#/components/TableLoader/TableLoader"; @@ -8,9 +8,9 @@ import { VersionRow } from "./VersionRow"; interface VersionsTableProps { activeVersionId: string; - versions?: TypesGen.TemplateVersion[]; - onPromoteClick?: (templateVersionId: string) => void; - onArchiveClick?: (templateVersionId: string) => void; + versions?: TemplateVersion[]; + onPromoteClick?: (version: TemplateVersion) => void; + onArchiveClick?: (version: TemplateVersion) => void; } export const VersionsTable: FC = ({ @@ -19,7 +19,7 @@ export const VersionsTable: FC = ({ onArchiveClick, onPromoteClick, }) => { - const latestVersionId = versions?.reduce( + const latestVersionId = versions?.reduce( (latestSoFar, against) => { if (against.job.status !== "succeeded") { return latestSoFar; @@ -34,7 +34,7 @@ export const VersionsTable: FC = ({ ? against : latestSoFar; }, - undefined as TypesGen.TemplateVersion | undefined, + undefined, )?.id; return (