Files
buzz/desktop/src/features/sidebar/ui/CreateChannelFormFields.tsx
Matt Toohey c3132c3ee9 feat(desktop): use segmented controls for channel creation (#6845)
## Summary

- replace the two-option Type and Visibility dropdowns in the Create
channel dialog with single-click segmented controls
- keep Expires after as a dropdown and preserve the existing dropdown
controls in edit and management dialogs
- update channel creation end-to-end coverage for the direct controls

## Before

Default Ongoing/Public state:

![Create channel before - default Ongoing and
Public](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6845/before-default.png)

Type dropdown open, showing the extra selection click:

![Create channel before - Type dropdown
open](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6845/before-type-dropdown.png)

## After

Default Ongoing/Public state:

![Create channel after - default Ongoing and
Public](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6845/after-default-reversed.png)

Temporary/Private state with the Expires after row visible:

![Create channel after - Temporary and
Private](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6845/after-temporary-private-reversed.png)

## Verification

- Biome and TypeScript checks pass
- 5,508 desktop unit tests pass
- 88 channel smoke tests pass
- source guards pass

---------

Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 12:14:35 +10:00

258 lines
8.5 KiB
TypeScript

import { ChevronDown, Plus } from "lucide-react";
import * as React from "react";
import { TemplateFormDialog } from "@/features/settings/ui/ChannelTemplatesSettingsCard";
import { cn } from "@/shared/lib/cn";
import { Button } from "@/shared/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/shared/ui/dropdown-menu";
import { Input } from "@/shared/ui/input";
import { Textarea } from "@/shared/ui/textarea";
import {
CHANNEL_FORM_FIELD_CONTROL_CLASS,
CHANNEL_FORM_FIELD_SHELL_CLASS,
} from "@/features/channels/ui/channelFormStyles";
import { ChannelPermissionsSettings } from "@/features/channels/ui/ChannelPermissionsSettings";
import { ChannelTypeSettings } from "@/features/channels/ui/ChannelTypeSettings";
import type { CreateChannelFormState } from "@/features/sidebar/lib/useCreateChannelForm";
const CREATE_LABEL_OPTIONAL_CLASS =
"ml-1 text-xs font-normal text-muted-foreground/50";
const NO_TEMPLATE_VALUE = "__no-template__";
export const CREATE_CHANNEL_FORM_ID = "create-channel-form";
/**
* The body of the create-channel form (name, description, visibility,
* optional template). Rendered inside both the standalone dialog and the
* "Add channel" browser's create mode. Wrap in a `<form>` with
* `id={CREATE_CHANNEL_FORM_ID}` and hook up `form.handleSubmit`.
*/
export function CreateChannelFormFields({
form,
}: {
form: CreateChannelFormState;
}) {
const { channelKind, kindLabel, isCreating } = form;
const [isCreateTemplateOpen, setIsCreateTemplateOpen] = React.useState(false);
const selectedTemplate = form.templates.find(
(template) => template.id === form.selectedTemplateId,
);
const selectedTemplatePersonaCount =
selectedTemplate?.agents.personas.length ?? 0;
const selectedTemplateTeamCount = selectedTemplate?.agents.teams.length ?? 0;
const selectedTemplateSummary = selectedTemplate
? [
form.visibility === "private" ? "Private" : "Open",
selectedTemplate.canvasTemplate ? "Canvas included" : null,
selectedTemplatePersonaCount > 0
? `${selectedTemplatePersonaCount} ${selectedTemplatePersonaCount === 1 ? "agent" : "agents"}`
: null,
selectedTemplateTeamCount > 0
? `${selectedTemplateTeamCount} ${selectedTemplateTeamCount === 1 ? "team" : "teams"}`
: null,
]
.filter(Boolean)
.join(" · ")
: null;
return (
<div className="space-y-5">
<div className="space-y-1.5">
<label
className="text-sm font-medium text-foreground"
htmlFor="create-channel-name"
>
Name
</label>
<div
className={cn(
"flex min-h-11 items-center px-3",
CHANNEL_FORM_FIELD_SHELL_CLASS,
)}
>
<Input
autoCapitalize="none"
autoComplete="off"
autoCorrect="off"
className={cn(
"h-8 px-0 py-0 leading-6",
CHANNEL_FORM_FIELD_CONTROL_CLASS,
)}
data-testid="create-channel-name"
disabled={isCreating}
id="create-channel-name"
onChange={(event) => form.setName(event.target.value)}
placeholder={
channelKind === "forum" ? "design-discussions" : "release-notes"
}
ref={form.nameInputRef}
spellCheck={false}
value={form.name}
/>
</div>
</div>
<div className="space-y-1.5">
<label
className="text-sm font-medium text-foreground"
htmlFor="create-channel-description"
>
Description
<span className={CREATE_LABEL_OPTIONAL_CLASS}>Optional</span>
</label>
<div className={CHANNEL_FORM_FIELD_SHELL_CLASS}>
<Textarea
className={cn(
"min-h-20 resize-none px-3 py-3 leading-5",
CHANNEL_FORM_FIELD_CONTROL_CLASS,
)}
data-testid="create-channel-description"
disabled={isCreating}
id="create-channel-description"
onChange={(event) => form.setDescription(event.target.value)}
placeholder={`What this ${kindLabel} is for`}
rows={2}
value={form.description}
/>
</div>
</div>
<ChannelTypeSettings
disabled={isCreating}
label="Type"
onTemporaryChange={form.setEphemeral}
onTtlSecondsChange={form.setTtlSeconds}
temporary={form.ephemeral}
testIdPrefix="create-channel"
ttlSeconds={form.ttlSeconds}
variant="segmented"
/>
<ChannelPermissionsSettings
disabled={isCreating}
onVisibilityChange={form.setVisibility}
testIdPrefix="create-channel"
visibility={form.visibility}
variant="segmented"
/>
<div
className="flex min-h-12 items-center justify-between gap-4 rounded-xl border border-input bg-background px-3 py-3"
data-testid="create-channel-template-container"
>
<span
className={cn(
"text-sm font-medium text-foreground",
isCreating && "opacity-50",
)}
>
Template
<span className={CREATE_LABEL_OPTIONAL_CLASS}>Optional</span>
</span>
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<Button
aria-label={`Template: ${selectedTemplate?.name ?? "None"}`}
className="-mr-2.5 ml-auto h-9 min-w-0 max-w-[60%] justify-end px-2.5 text-right text-sm font-medium text-foreground hover:bg-muted/50"
data-testid="create-channel-template"
disabled={isCreating}
id="create-channel-template"
type="button"
variant="ghost"
>
<span className="truncate text-right">
{selectedTemplate?.name ?? "None"}
</span>
<ChevronDown className="size-4 shrink-0 text-muted-foreground/70" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
onCloseAutoFocus={(event) => event.preventDefault()}
style={{
minWidth: "var(--radix-dropdown-menu-trigger-width)",
}}
>
<DropdownMenuRadioGroup
onValueChange={(templateId) =>
form.handleTemplateChange(
templateId === NO_TEMPLATE_VALUE ? "" : templateId,
)
}
value={form.selectedTemplateId ?? NO_TEMPLATE_VALUE}
>
<DropdownMenuRadioItem value={NO_TEMPLATE_VALUE}>
None
</DropdownMenuRadioItem>
{form.templates.map((template) => (
<DropdownMenuRadioItem key={template.id} value={template.id}>
{template.name}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
<DropdownMenuSeparator />
<DropdownMenuItem onSelect={() => setIsCreateTemplateOpen(true)}>
<Plus className="size-4" />
Create new channel template…
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<TemplateFormDialog
onCreated={form.handleTemplateCreated}
onOpenChange={setIsCreateTemplateOpen}
open={isCreateTemplateOpen}
template={null}
/>
</div>
{selectedTemplateSummary ? (
<p
className="-mt-3 px-3 text-xs text-muted-foreground"
data-testid="create-channel-template-summary"
>
{selectedTemplateSummary}
</p>
) : null}
{form.errorMessage ? (
<p className="text-sm text-destructive">{form.errorMessage}</p>
) : null}
</div>
);
}
/**
* Footer for the create-channel form. The submit button is bound to the form
* via `form={CREATE_CHANNEL_FORM_ID}`.
*/
export function CreateChannelFormFooter({
form,
submitLabel,
}: {
form: CreateChannelFormState;
submitLabel?: string;
}) {
const { isCreating, kindLabel } = form;
return (
<div className="flex w-full items-center justify-end gap-3">
<Button
data-testid="create-channel-submit"
disabled={!form.canSubmit}
form={CREATE_CHANNEL_FORM_ID}
type="submit"
>
{isCreating ? "Creating..." : (submitLabel ?? `Create ${kindLabel}`)}
</Button>
</div>
);
}