Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 74 additions & 47 deletions desktop/src/features/channels/ui/ChannelPermissionsSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ChevronDown } from "lucide-react";
import { ChevronDown, Globe, Lock } from "lucide-react";

import type { ChannelVisibility } from "@/shared/api/types";
import { Button } from "@/shared/ui/button";
Expand All @@ -10,75 +10,102 @@ import {
DropdownMenuTrigger,
} from "@/shared/ui/dropdown-menu";
import { cn } from "@/shared/lib/cn";
import { SegmentedControl } from "@/shared/ui/segmented-control";

const VISIBILITY_OPTIONS = [
{ value: "private", label: "Private", Icon: Lock },
{ value: "open", label: "Public", Icon: Globe },
] as const;

export function ChannelPermissionsSettings({
disabled,
onVisibilityChange,
testIdPrefix,
visibility,
variant = "dropdown",
}: {
disabled?: boolean;
onVisibilityChange: (visibility: ChannelVisibility) => void;
testIdPrefix: string;
visibility: ChannelVisibility;
variant?: "dropdown" | "segmented";
}) {
const visibilityLabel = visibility === "private" ? "Private" : "Public";

return (
<div
className={cn(
"flex min-h-12 items-center justify-between gap-4 rounded-xl border border-input bg-background px-3 py-3",
disabled && "opacity-50",
disabled && variant === "dropdown" && "opacity-50",
)}
data-testid={`${testIdPrefix}-permissions-container`}
>
<span className="text-sm font-medium text-foreground">Visibility</span>
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<Button
aria-label={`Visibility: ${visibilityLabel}`}
className="-mr-2.5 ml-auto h-9 w-fit justify-end px-2.5 text-right text-sm font-medium text-foreground hover:bg-muted/50"
data-testid={`${testIdPrefix}-permissions`}
disabled={disabled}
type="button"
variant="ghost"
>
<span aria-live="polite" className="text-right">
{visibilityLabel}
</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={(nextVisibility) =>
onVisibilityChange(
nextVisibility === "private" ? "private" : "open",
)
}
value={visibility}
>
<DropdownMenuRadioItem
data-testid={`${testIdPrefix}-permissions-option-open`}
value="open"
<span
className={cn(
"text-sm font-medium text-foreground",
disabled && variant === "segmented" && "opacity-50",
)}
>
Visibility
</span>
{variant === "segmented" ? (
<SegmentedControl
disabled={disabled}
legend="Visibility"
onValueChange={onVisibilityChange}
optionTestIdPrefix={`${testIdPrefix}-permissions-option`}
options={VISIBILITY_OPTIONS}
testId={`${testIdPrefix}-permissions`}
value={visibility}
/>
) : (
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<Button
aria-label={`Visibility: ${visibilityLabel}`}
className="-mr-2.5 ml-auto h-9 w-fit justify-end px-2.5 text-right text-sm font-medium text-foreground hover:bg-muted/50"
data-testid={`${testIdPrefix}-permissions`}
disabled={disabled}
type="button"
variant="ghost"
>
Public
</DropdownMenuRadioItem>
<DropdownMenuRadioItem
data-testid={`${testIdPrefix}-permissions-option-private`}
value="private"
<span aria-live="polite" className="text-right">
{visibilityLabel}
</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={(nextVisibility) =>
onVisibilityChange(
nextVisibility === "private" ? "private" : "open",
)
}
value={visibility}
>
Private
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenuRadioItem
data-testid={`${testIdPrefix}-permissions-option-open`}
value="open"
>
Public
</DropdownMenuRadioItem>
<DropdownMenuRadioItem
data-testid={`${testIdPrefix}-permissions-option-private`}
value="private"
>
Private
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
);
}
61 changes: 47 additions & 14 deletions desktop/src/features/channels/ui/ChannelTypeSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ChevronDown } from "lucide-react";
import { ChevronDown, ClockFading, Hash } from "lucide-react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";

import {
Expand All @@ -11,6 +11,7 @@ import {
} from "@/features/channels/lib/ephemeralChannel";
import { useIsProjectHomeChannel } from "@/features/projects/lib/projectHomeChannel";
import type { Channel } from "@/shared/api/types";
import { cn } from "@/shared/lib/cn";
import { Button } from "@/shared/ui/button";
import {
DropdownMenu,
Expand All @@ -19,9 +20,15 @@ import {
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/shared/ui/dropdown-menu";
import { SegmentedControl } from "@/shared/ui/segmented-control";
import { EditableInfoFieldRow } from "./ChannelManagementSheetRows";
import { ChannelTypePicker } from "./ChannelTypePicker";

const CHANNEL_TYPE_OPTIONS = [
{ value: "temporary", label: "Temporary", Icon: ClockFading },
{ value: "ongoing", label: "Ongoing", Icon: Hash },
] as const;

const EPHEMERAL_TIMEOUT_OPTIONS = [
{ label: "30 minutes", seconds: 30 * 60 },
{ label: "1 hour", seconds: 60 * 60 },
Expand Down Expand Up @@ -76,6 +83,7 @@ export function ChannelTypeSettings({
temporary,
testIdPrefix,
ttlSeconds,
variant = "dropdown",
}: {
channelId?: string | null;
disabled?: boolean;
Expand All @@ -87,6 +95,7 @@ export function ChannelTypeSettings({
temporary: boolean;
testIdPrefix: string;
ttlSeconds: number;
variant?: "dropdown" | "segmented";
}) {
const projectHome = useIsProjectHomeChannel(channelId);
const lifecycle = channelLifecycle({ projectHome, temporary });
Expand Down Expand Up @@ -116,18 +125,39 @@ export function ChannelTypeSettings({
className="flex items-center justify-between gap-3 px-3 py-3"
data-testid={`${testIdPrefix}-channel-type-row`}
>
<span className="text-sm font-medium text-foreground">{label}</span>
<ChannelTypePicker
align="end"
allowProject={projectHome}
className="-mr-2.5"
disabled={disabled}
lifecycle={lifecycle}
onLifecycleChange={(next) => onTemporaryChange(next === "temporary")}
onOpenChange={onOpenChange}
open={open}
testId={`${testIdPrefix}-channel-type`}
/>
<span
className={cn(
"text-sm font-medium text-foreground",
disabled && variant === "segmented" && "opacity-50",
)}
>
{label}
</span>
{variant === "segmented" ? (
<SegmentedControl
disabled={disabled}
legend="Channel type"
onValueChange={(value) => onTemporaryChange(value === "temporary")}
optionTestIdPrefix={`${testIdPrefix}-channel-type-option`}
options={CHANNEL_TYPE_OPTIONS}
testId={`${testIdPrefix}-channel-type`}
value={temporary ? "temporary" : "ongoing"}
/>
) : (
<ChannelTypePicker
align="end"
allowProject={projectHome}
className="-mr-2.5"
disabled={disabled}
lifecycle={lifecycle}
onLifecycleChange={(next) =>
onTemporaryChange(next === "temporary")
}
onOpenChange={onOpenChange}
open={open}
testId={`${testIdPrefix}-channel-type`}
/>
)}
</div>
<AnimatePresence initial={false}>
{temporary && !projectHome ? (
Expand All @@ -144,7 +174,10 @@ export function ChannelTypeSettings({
data-testid={`${testIdPrefix}-ephemeral-settings`}
>
<label
className="text-sm font-medium"
className={cn(
"text-sm font-medium",
disabled && variant === "segmented" && "opacity-50",
)}
htmlFor={`${testIdPrefix}-ttl`}
>
Expires after
Expand Down
6 changes: 0 additions & 6 deletions desktop/src/features/sidebar/lib/useCreateChannelForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,6 @@ export type CreateChannelFormState = {
setEphemeral: (value: boolean) => void;
ttlSeconds: number;
setTtlSeconds: (value: number) => void;
typePopoverOpen: boolean;
setTypePopoverOpen: (open: boolean) => void;
errorMessage: string | null;
selectedTemplateId: string | null;
handleTemplateChange: (templateId: string) => void;
Expand Down Expand Up @@ -79,7 +77,6 @@ export function useCreateChannelForm({
const [selectedTemplateId, setSelectedTemplateId] = React.useState<
string | null
>(null);
const [typePopoverOpen, setTypePopoverOpen] = React.useState(false);
const nameInputRef = React.useRef<HTMLInputElement>(null);
const visibilityTouchedRef = React.useRef(false);

Expand All @@ -97,7 +94,6 @@ export function useCreateChannelForm({
setTtlSeconds(DEFAULT_EPHEMERAL_TTL_SECONDS);
setErrorMessage(null);
setSelectedTemplateId(null);
setTypePopoverOpen(false);
visibilityTouchedRef.current = false;

if (!autoFocusName) return;
Expand Down Expand Up @@ -211,8 +207,6 @@ export function useCreateChannelForm({
setEphemeral,
ttlSeconds,
setTtlSeconds,
typePopoverOpen,
setTypePopoverOpen,
errorMessage,
selectedTemplateId,
handleTemplateChange,
Expand Down
16 changes: 9 additions & 7 deletions desktop/src/features/sidebar/ui/CreateChannelFormFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,30 +129,32 @@ export function CreateChannelFormFields({
<ChannelTypeSettings
disabled={isCreating}
label="Type"
onOpenChange={form.setTypePopoverOpen}
onTemporaryChange={form.setEphemeral}
onTtlSecondsChange={form.setTtlSeconds}
open={form.typePopoverOpen}
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={cn(
"flex min-h-12 items-center justify-between gap-4 rounded-xl border border-input bg-background px-3 py-3",
isCreating && "opacity-50",
)}
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="text-sm font-medium text-foreground">
<span
className={cn(
"text-sm font-medium text-foreground",
isCreating && "opacity-50",
)}
>
Template
<span className={CREATE_LABEL_OPTIONAL_CLASS}>Optional</span>
</span>
Expand Down
4 changes: 4 additions & 0 deletions desktop/src/shared/ui/segmented-control.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const SIZE_CLASSES: Record<SegmentedControlSize, string> = {
/** A mutually exclusive control with equal-width, optionally scrubbable options. */
export function SegmentedControl<Value extends string>({
className,
disabled = false,
indicatorTestId,
legend,
onPreviewChange,
Expand All @@ -30,6 +31,7 @@ export function SegmentedControl<Value extends string>({
value,
}: {
className?: string;
disabled?: boolean;
indicatorTestId?: string;
legend: string;
onPreviewChange?: (value: Value | null) => void;
Expand Down Expand Up @@ -161,10 +163,12 @@ export function SegmentedControl<Value extends string>({
"relative isolate h-8 max-w-full shrink-0 overflow-hidden rounded-md bg-muted/45 p-0.5",
SIZE_CLASSES[size],
onPreviewChange && "touch-none select-none cursor-ew-resize",
"disabled:pointer-events-none disabled:opacity-50",
className,
)}
data-slot="segmented-control"
data-testid={testId}
disabled={disabled}
onLostPointerCapture={handleLostPointerCapture}
onPointerCancel={handlePointerCancel}
onPointerDown={handlePointerDown}
Expand Down
Loading
Loading