feat(site/src/pages/TemplateBuilder): deselect modules using button in main content area instead of sidebar (#27113)

closes DEVEX-588

Prototyped in #27077, broken off into a separate PR to make this work
easier to track

## changes

- Reveals the previously hidden trash can icon within
`ModuleConfiguration` (main content area)
- Removes the "x" icons from `ModuleSelection` (sidebar)

## context

@tracyjohnsonux and I decided [in
Slack](https://codercom.slack.com/archives/C0AUKB54P0E/p1783456607073329?thread_ts=1783450189.570979&cid=C0AUKB54P0E)
that it would be a better UX to move the deletion action from the "x"
icons in the sidebar to the trash can icons in the main content area.
This change has the benefits of

1. making it harder to delete modules accidentally
2. removing the responsibility of deletion from the items in
`ModuleSelection`
- interacting with these items will serve only to navigate to
configuring that module (DEVEX-587, to be done in a separate PR)

<img width="1840" height="1191" alt="image"
src="https://github.com/user-attachments/assets/4571ea2f-75cf-4cd7-b626-1826eea83bdf"
/>
This commit is contained in:
Andrew Aquino
2026-07-09 10:48:14 -07:00
committed by GitHub
parent 20ee0068e6
commit 8a095d3b38
4 changed files with 10 additions and 30 deletions
@@ -26,6 +26,7 @@ interface ModuleSettingsStepProps {
moduleId: string,
variables: Record<string, string>,
) => void;
onRemoveModule: (moduleId: string) => void;
}
function variableToField(
@@ -107,6 +108,7 @@ export const ModuleSettingsStep: FC<ModuleSettingsStepProps> = ({
selectedModuleIds,
moduleVariables,
onChangeModuleVariables,
onRemoveModule,
}) => {
const { data } = useQuery(templateBuilderModules(baseId));
const modules = data?.modules ?? [];
@@ -157,6 +159,7 @@ export const ModuleSettingsStep: FC<ModuleSettingsStepProps> = ({
detailsUrl={moduleDetailsUrl(mod.id)}
fields={requiredFields}
optionalFields={optionalFields}
onRemove={() => onRemoveModule(mod.id)}
/>
{sensitiveVars.length > 0 && (
@@ -1,13 +1,10 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect, fn, within } from "storybook/test";
import { expect, within } from "storybook/test";
import { SelectionSummary } from "./SelectionSummary";
const meta: Meta<typeof SelectionSummary> = {
title: "pages/TemplateBuilder/SelectionSummary",
component: SelectionSummary,
args: {
onDeselectModule: fn(),
},
};
export default meta;
@@ -1,8 +1,6 @@
import { cva } from "class-variance-authority";
import { XIcon } from "lucide-react";
import { createContext, type PropsWithChildren, useContext } from "react";
import { Avatar } from "#/components/Avatar/Avatar";
import { Button } from "#/components/Button/Button";
import { cn } from "#/utils/cn";
type Variant = "complete" | "current" | "upcoming" | null | undefined;
@@ -24,14 +22,12 @@ type SelectionSummaryProps = {
currentStep: number;
selectedTemplate?: SelectedTemplate;
selectedModules?: SelectedModule[];
onDeselectModule: (moduleId: string) => void;
};
export const SelectionSummary: React.FC<SelectionSummaryProps> = ({
currentStep,
selectedTemplate,
selectedModules,
onDeselectModule,
}) => {
const variant = (step: number) => {
if (currentStep === step) return "current";
@@ -53,10 +49,7 @@ export const SelectionSummary: React.FC<SelectionSummaryProps> = ({
<VariantContext.Provider value={variant(2)}>
<StepIndicator step={2}>Modules</StepIndicator>
{selectedModules ? (
<ModuleSelection
modules={selectedModules}
onDeselectModule={onDeselectModule}
/>
<ModuleSelection modules={selectedModules} />
) : (
<StepDivider />
)}
@@ -161,19 +154,15 @@ const BaseTemplateSelection: React.FC<BaseTemplateSelectionProps> = ({
type ModuleSelectionProps = {
modules: SelectedModule[];
onDeselectModule: (moduleId: string) => void;
};
const ModuleSelection: React.FC<ModuleSelectionProps> = ({
modules,
onDeselectModule,
}) => {
const ModuleSelection: React.FC<ModuleSelectionProps> = ({ modules }) => {
return (
<StepDivider className="max-h-72 overflow-y-auto">
{modules.map((module) => (
<div
key={module.id}
className="group flex items-start justify-between p-1 mb-1 rounded-sm hover:bg-surface-secondary"
className="group flex items-start justify-between p-1 mb-1 rounded-sm"
>
<div className="h-[1lh] content-center">
<Avatar src={module.iconUrl} size="sm" variant="icon" />
@@ -181,17 +170,6 @@ const ModuleSelection: React.FC<ModuleSelectionProps> = ({
<span className="flex-1 ml-2 text-content-secondary">
{module.name}
</span>
<div className="h-[1lh] content-center">
<Button
size="xs"
variant="subtle"
className="flex opacity-0 group-hover:opacity-100 focus-visible:opacity-100"
onClick={() => onDeselectModule(module.id)}
aria-label="Deselect module"
>
<XIcon className="size-4" />
</Button>
</div>
</div>
))}
</StepDivider>
@@ -162,6 +162,7 @@ export const TemplateBuilderPageView: FC<TemplateBuilderPageViewProps> = ({
moduleVarMap,
createError,
handleProvisionerStatusChange,
handleDeselectModule,
)}
</div>
@@ -199,7 +200,6 @@ export const TemplateBuilderPageView: FC<TemplateBuilderPageViewProps> = ({
? state.selectedModules
: undefined
}
onDeselectModule={handleDeselectModule}
/>
</div>
</div>
@@ -214,6 +214,7 @@ function renderStepContent(
moduleVarMap: Record<string, Record<string, string>>,
createError: Error | null,
onProvisionerStatusChange: (value: boolean | undefined) => void,
onRemoveModule: (moduleId: string) => void,
): ReactNode {
switch (stepId) {
case "base-infra":
@@ -259,6 +260,7 @@ function renderStepContent(
variables,
})
}
onRemoveModule={onRemoveModule}
/>
);
case "customizations":