mirror of
https://github.com/coder/coder.git
synced 2026-09-22 05:05:20 +08:00
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:
+6
-7
@@ -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…
|
||||
@@ -105,7 +105,7 @@ export const VersionRow: FC<VersionRowProps> = ({
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onPromoteClick?.(version.id);
|
||||
onPromoteClick?.(version);
|
||||
}}
|
||||
>
|
||||
Promote…
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user