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<void>`

<img width="772" height="152" alt="image"
src="https://github.com/user-attachments/assets/0063ebdc-09a3-4041-bfa0-d4c70b57159b"
/>
This commit is contained in:
Jake Howell
2026-08-06 04:41:46 +00:00
committed by GitHub
parent 2484f4b047
commit 6c8a8647f6
4 changed files with 89 additions and 101 deletions
+6 -7
View File
@@ -1166,19 +1166,18 @@ class ApiMethods {
return response.data;
};
archiveTemplateVersion = async (templateVersionId: string) => {
const response = await this.axios.post<TypesGen.TemplateVersion>(
archiveTemplateVersion = async (templateVersionId: string): Promise<void> => {
await this.axios.post(
`/api/v2/templateversions/${templateVersionId}/archive`,
);
return response.data;
};
unarchiveTemplateVersion = async (templateVersionId: string) => {
const response = await this.axios.post<TypesGen.TemplateVersion>(
unarchiveTemplateVersion = async (
templateVersionId: string,
): Promise<void> => {
await this.axios.post(
`/api/v2/templateversions/${templateVersionId}/unarchive`,
);
return response.data;
};
/**
@@ -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 (
<>
<title>{getTemplatePageTitle("Versions", template)}</title>
@@ -112,44 +56,89 @@ const TemplateVersionsPage = () => {
<VersionsTable
versions={data}
onPromoteClick={
permissions.canUpdateTemplate
? setSelectedVersionIdToPromote
: undefined
permissions.canUpdateTemplate ? setVersionToPromote : undefined
}
onArchiveClick={
permissions.canUpdateTemplate
? setSelectedVersionIdToArchive
: undefined
permissions.canUpdateTemplate ? setVersionToArchive : undefined
}
activeVersionId={latestActiveVersion}
/>
{/* Promote confirm */}
<ConfirmDialog
type="info"
hideCancel={false}
open={selectedVersionIdToPromote !== undefined}
onConfirm={() => {
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{" "}
<strong>{versionToPromote?.name}</strong>? Workspaces will be
prompted to Update to this version once promoted.
</>
}
/>
{/* Archive Confirm */}
<ConfirmDialog
type="info"
hideCancel={false}
open={selectedVersionIdToArchive !== undefined}
onConfirm={() => {
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{" "}
<strong>{versionToArchive?.name}</strong>? This is reversible.
Archived versions cannot be used by workspaces.
</>
}
/>
</>
);
@@ -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<VersionRowProps> = ({
@@ -91,7 +91,7 @@ export const VersionRow: FC<VersionRowProps> = ({
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onArchiveClick?.(version.id);
onArchiveClick?.(version);
}}
>
Archive&hellip;
@@ -105,7 +105,7 @@ export const VersionRow: FC<VersionRowProps> = ({
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onPromoteClick?.(version.id);
onPromoteClick?.(version);
}}
>
Promote&hellip;
@@ -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<VersionsTableProps> = ({
@@ -19,7 +19,7 @@ export const VersionsTable: FC<VersionsTableProps> = ({
onArchiveClick,
onPromoteClick,
}) => {
const latestVersionId = versions?.reduce(
const latestVersionId = versions?.reduce<TemplateVersion | undefined>(
(latestSoFar, against) => {
if (against.job.status !== "succeeded") {
return latestSoFar;
@@ -34,7 +34,7 @@ export const VersionsTable: FC<VersionsTableProps> = ({
? against
: latestSoFar;
},
undefined as TypesGen.TemplateVersion | undefined,
undefined,
)?.id;
return (