feat: team users clean up (#4448)

Co-authored-by: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com>
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
Co-authored-by: Matti Nannt <mail@matthiasnannt.com>
This commit is contained in:
Piyush Gupta
2024-12-18 17:50:50 +05:30
committed by GitHub
parent 7b11ef9b40
commit 15f36651d8
112 changed files with 3292 additions and 4077 deletions
@@ -0,0 +1,30 @@
import { type VariantProps, cva } from "class-variance-authority";
import * as React from "react";
import { cn } from "@formbricks/lib/cn";
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
}
export { Badge, badgeVariants };
@@ -1,130 +1,159 @@
import {
Command,
CommandEmpty,
CommandGroup,
CommandItem,
CommandList,
} from "@/modules/ui/components/command";
import clsx from "clsx";
import { ChevronDownIcon, ChevronUpIcon, XIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useRef, useState } from "react";
import { cn } from "@formbricks/lib/cn";
import { useClickOutside } from "@formbricks/lib/utils/hooks/useClickOutside";
"use client";
import { Command, CommandGroup, CommandItem, CommandList } from "@/modules/ui/components/command";
import { Badge } from "@/modules/ui/components/multi-select/badge";
import { Command as CommandPrimitive } from "cmdk";
import { X } from "lucide-react";
import * as React from "react";
interface TOption<T> {
label: string;
value: T;
label: string;
}
interface MultiSelectProps<T extends string, K extends TOption<T>["value"] | TOption<T>["value"][]> {
interface MultiSelectProps<T extends string, K extends TOption<T>["value"][]> {
options: TOption<T>[];
isMultiple?: boolean;
disabled?: boolean;
isDisabledComboBox?: boolean;
value?: K;
onChange: (value: K) => void;
onChange?: (selected: K) => void;
disabled?: boolean;
placeholder?: string;
}
export const MultiSelect = <T extends string, K extends TOption<T>["value"] | TOption<T>["value"][]>({
options,
isMultiple = true,
disabled,
isDisabledComboBox,
value,
onChange,
}: MultiSelectProps<T, K>) => {
const [open, setOpen] = useState(false);
const t = useTranslations();
const isOptionSelected = (optionValue: T) => {
if (Array.isArray(value)) {
return value.includes(optionValue);
}
return value === optionValue;
};
export function MultiSelect<T extends string, K extends TOption<T>["value"][]>(
props: MultiSelectProps<T, K>
) {
const { options, value, onChange, disabled = false, placeholder = "Select options..." } = props;
const handleSelect = (optionValue: T) => {
if (isMultiple) {
if (Array.isArray(value)) {
if (isOptionSelected(optionValue)) {
onChange(value.filter((v) => v !== optionValue) as K);
} else {
onChange([...value, optionValue] as K);
}
} else {
onChange([optionValue] as K);
}
} else {
onChange(optionValue as K);
}
setOpen(false);
};
const inputRef = React.useRef<HTMLInputElement>(null);
const filteredOptions = options.filter((option) => {
if (Array.isArray(value)) {
return !value.includes(option.value);
const [selected, setSelected] = React.useState<TOption<T>[]>(() => {
if (value) {
return value.map((val) => options.find((o) => o.value === val)).filter((o): o is TOption<T> => !!o);
}
return option.value !== value;
return [];
});
const commandRef = useRef<HTMLDivElement>(null);
useClickOutside(commandRef, () => setOpen(false));
const [open, setOpen] = React.useState(false);
const [inputValue, setInputValue] = React.useState("");
React.useEffect(() => {
if (value) {
setSelected(
value.map((val) => options.find((o) => o.value === val)).filter((o): o is TOption<T> => !!o)
);
}
}, [value, options]);
const handleUnselect = React.useCallback(
(option: TOption<T>) => {
if (disabled) return;
setSelected((prev) => {
const newSelected = prev.filter((s) => s.value !== option.value);
onChange?.(newSelected.map((s) => s.value) as K);
return newSelected;
});
},
[onChange, disabled]
);
const handleKeyDown = React.useCallback(
(e: React.KeyboardEvent<HTMLDivElement>) => {
const input = inputRef.current;
if (!input || disabled) return;
if ((e.key === "Delete" || e.key === "Backspace") && input.value === "") {
setSelected((prev) => {
const newSelected = [...prev];
newSelected.pop();
onChange?.(newSelected.map((s) => s.value) as K);
return newSelected;
});
}
if (e.key === "Escape") {
input.blur();
}
},
[onChange, disabled]
);
const selectableOptions = React.useMemo(() => {
return options
.filter((o) => !selected.some((s) => s.value === o.value))
.filter((o) => {
if (!inputValue) return true;
return o.label.toLowerCase().includes(inputValue.toLowerCase());
});
}, [options, selected, inputValue]);
return (
<Command ref={commandRef} className="overflow-visible bg-transparent" id="multi-select-dropdown">
<Command
onKeyDown={handleKeyDown}
className={`overflow-visible bg-white ${disabled ? "cursor-not-allowed opacity-50" : ""}`}>
<div
onClick={() => !disabled && !isDisabledComboBox && setOpen((open) => !open)}
className={clsx(
"group flex items-center justify-between rounded-md rounded-l-none border bg-white px-3 py-2 text-sm",
disabled || isDisabledComboBox ? "opacity-50" : "cursor-pointer"
)}>
{value && (Array.isArray(value) ? value.length > 0 : !!value) ? (
!Array.isArray(value) ? (
<p className="text-slate-600">{options.find((option) => option.value === value)?.label}</p>
) : (
<div className="no-scrollbar flex gap-3 overflow-auto" onClick={(e) => e.stopPropagation()}>
{value.map((val) => (
<button
type="button"
key={val}
onClick={() => handleSelect(val)}
className="w-30 flex items-center whitespace-nowrap bg-slate-100 px-2 text-slate-600">
{options.find((option) => option.value === val)?.label}
<XIcon width={14} height={14} className="ml-2" />
</button>
))}
</div>
)
) : (
<p className="text-slate-400">{t("common.select")}...</p>
)}
<div>
{open ? (
<ChevronUpIcon className="ml-2 h-4 w-4 opacity-50" />
) : (
<ChevronDownIcon className="ml-2 h-4 w-4 opacity-50" />
)}
className={`border-input ring-offset-background group rounded-md border px-3 py-2 text-sm focus-within:ring-2 focus-within:ring-offset-2 ${
disabled ? "pointer-events-none" : "focus-within:ring-ring"
}`}>
<div className="flex flex-wrap gap-1">
{selected.map((option) => (
<Badge key={option.value} className="rounded-md">
{option.label}
<button
className="ring-offset-background focus:ring-ring ml-1 rounded-full outline-none focus:ring-2 focus:ring-offset-2"
onKeyDown={(e) => {
if (e.key === "Enter") {
handleUnselect(option);
}
}}
onMouseDown={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onClick={() => handleUnselect(option)}>
<X className="text-muted-foreground hover:text-foreground h-3 w-3" />
</button>
</Badge>
))}
<CommandPrimitive.Input
ref={inputRef}
value={inputValue}
onValueChange={setInputValue}
onBlur={() => setOpen(false)}
onFocus={() => setOpen(true)}
placeholder={placeholder}
disabled={disabled}
className="placeholder:text-muted-foreground h-5 flex-1 border-0 bg-transparent pl-2 outline-none"
/>
</div>
</div>
<div className="relative mt-2 h-full">
{open && (
<div className="animate-in bg-popover absolute top-0 z-10 max-h-52 w-full overflow-auto rounded-md border bg-white outline-none">
<CommandList>
<CommandEmpty>{t("common.no_result_found")}</CommandEmpty>
<CommandGroup>
{filteredOptions.map((o, idx) => (
<div className="relative mt-2">
<CommandList>
{open && selectableOptions.length > 0 && !disabled && (
<div className="text-popover-foreground animate-in absolute top-0 z-10 max-h-32 w-full rounded-md border bg-white shadow-md outline-none">
<CommandGroup className="h-full overflow-auto">
{selectableOptions.map((option) => (
<CommandItem
key={o.value}
onSelect={() => handleSelect(o.value)}
className={cn("cursor-pointer", `option-${idx + 1}`)}>
{o.label}
key={option.value}
onMouseDown={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onSelect={() => {
if (disabled) return;
const newSelected = [...selected, option];
setSelected(newSelected);
onChange?.(newSelected.map((o) => o.value) as K);
setInputValue("");
}}
className="cursor-pointer">
{option.label}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</div>
)}
</div>
)}
</CommandList>
</div>
</Command>
);
};
}
@@ -18,7 +18,7 @@ const SelectTrigger = React.forwardRef<
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-slate-300 bg-transparent px-3 py-2 text-sm placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50",
"flex h-10 w-full items-center justify-between rounded-md border border-slate-300 bg-transparent px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 data-[placeholder]:text-slate-400",
className
)}
{...props}>
@@ -0,0 +1,27 @@
import { Alert, AlertDescription } from "@/modules/ui/components/alert";
import { KeyIcon } from "lucide-react";
import Link from "next/link";
interface UpgradePlanNoticeProps {
message: string;
url: string;
textForUrl: string;
}
export const UpgradePlanNotice = ({ message, url, textForUrl }: UpgradePlanNoticeProps) => {
return (
<Alert className="flex gap-2 bg-slate-50 p-2 [&:has(svg)]:pl-3">
<div className="flex h-5 w-5 items-center justify-center rounded-sm border border-slate-200 bg-white">
<KeyIcon className="h-3 w-3 text-slate-900" />
</div>
<AlertDescription>
<span className="mr-1 text-slate-600">{message}</span>
<span className="underline">
<Link href={url} target="_blank">
{textForUrl}
</Link>
</span>
</AlertDescription>
</Alert>
);
};