АРХ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: миграция form и settings select-пикеров на SelectionDropdown

This commit is contained in:
DCCONSTRUCTIONS 2026-04-22 13:59:29 +03:00
parent 8fa5de24eb
commit ecb31a78f9
13 changed files with 237 additions and 230 deletions

View File

@ -9,7 +9,7 @@ import { Controller, useFormContext } from "react-hook-form";
import { NETWORK_CHOICES, ETabIndices } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import type { IProject } from "@plane/types";
import { CustomSelect } from "@plane/ui";
import { SelectionDropdown } from "@/components/common/selection-dropdown";
import { getTabIndex } from "@plane/utils";
// components
import { MemberDropdown } from "@/components/dropdowns/member/dropdown";
@ -36,10 +36,22 @@ function ProjectAttributes(props: Props) {
return (
<div className="h-7 flex-shrink-0" tabIndex={getIndex("network")}>
<CustomSelect
value={value}
onChange={onChange}
label={
<SelectionDropdown
options={NETWORK_CHOICES.map((network) => ({
key: String(network.key),
title: (
<div className="flex items-start gap-2">
<ProjectNetworkIcon iconKey={network.iconKey} className="h-3.5 w-3.5" />
<div className="-mt-1">
<p>{t(network.i18n_label)}</p>
<p className="text-11 text-placeholder">{t(network.description)}</p>
</div>
</div>
),
isChecked: value === network.key,
onClick: () => onChange(network.key),
}))}
menuButton={
<div className="flex h-full items-center gap-1">
{currentNetwork ? (
<>
@ -52,23 +64,9 @@ function ProjectAttributes(props: Props) {
</div>
}
placement="bottom-start"
className="h-10"
buttonClassName={projectAttributeChipClassName}
noChevron
menuButtonWrapperClassName={projectAttributeChipClassName}
tabIndex={getIndex("network")}
>
{NETWORK_CHOICES.map((network) => (
<CustomSelect.Option key={network.key} value={network.key}>
<div className="flex items-start gap-2">
<ProjectNetworkIcon iconKey={network.iconKey} className="h-3.5 w-3.5" />
<div className="-mt-1">
<p>{t(network.i18n_label)}</p>
<p className="text-11 text-placeholder">{t(network.description)}</p>
</div>
</div>
</CustomSelect.Option>
))}
</CustomSelect>
/>
</div>
);
}}

View File

@ -21,6 +21,10 @@ export type TSelectionDropdownOption = {
type Props = {
disabled?: boolean;
dropdownClassName?: string;
dropdownContentClassName?: string;
footerContent?: ReactNode;
headerContent?: ReactNode;
menuButton: ReactNode | ((props: { open: boolean }) => ReactNode);
menuButtonWrapperClassName?: string;
options: TSelectionDropdownOption[];
@ -30,7 +34,19 @@ type Props = {
};
export function SelectionDropdown(props: Props) {
const { disabled = false, menuButton, menuButtonWrapperClassName, options, placement = "bottom-start", tabIndex, title } = props;
const {
disabled = false,
dropdownClassName,
dropdownContentClassName,
footerContent,
headerContent,
menuButton,
menuButtonWrapperClassName,
options,
placement = "bottom-start",
tabIndex,
title,
} = props;
const renderedOptions = options.filter((option) => option.shouldRender !== false);
@ -41,9 +57,12 @@ export function SelectionDropdown(props: Props) {
placement={placement}
disabled={disabled}
tabIndex={tabIndex}
dropdownClassName={dropdownClassName}
dropdownContentClassName={dropdownContentClassName}
>
{({ closeDropdown }) => (
<div className="vertical-scrollbar relative scrollbar-sm h-full w-full overflow-y-auto px-2.5 py-2">
{headerContent}
{title && (
<div className="sticky top-0 z-[1] bg-[rgba(8,8,11,0.92)] pb-1 pt-0.5 text-caption-sm-medium text-placeholder backdrop-blur-xl">
{title}
@ -65,6 +84,7 @@ export function SelectionDropdown(props: Props) {
/>
))}
</div>
{footerContent}
</div>
)}
</FiltersDropdown>

View File

@ -23,6 +23,8 @@ type Props = {
menuButton?: React.ReactNode | ((props: { open: boolean }) => React.ReactNode);
menuButtonWrapperClassName?: string;
isFiltersApplied?: boolean;
dropdownClassName?: string;
dropdownContentClassName?: string;
};
export function FiltersDropdown(props: Props) {
@ -37,6 +39,8 @@ export function FiltersDropdown(props: Props) {
menuButton,
menuButtonWrapperClassName,
isFiltersApplied = false,
dropdownClassName,
dropdownContentClassName,
} = props;
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | HTMLDivElement | null>(null);
@ -110,12 +114,16 @@ export function FiltersDropdown(props: Props) {
{/** translate-y-0 is a hack to create new stacking context. Required for safari */}
<Popover.Panel className="fixed z-[760] translate-y-0">
<div
className="nodedc-dropdown-surface my-1 overflow-hidden"
className={`nodedc-dropdown-surface my-1 overflow-hidden ${dropdownClassName ?? ""}`}
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<div className="flex max-h-[30rem] w-[18.75rem] flex-col overflow-hidden lg:max-h-[37.5rem]">
<div
className={`flex max-h-[30rem] w-[18.75rem] flex-col overflow-hidden lg:max-h-[37.5rem] ${
dropdownContentClassName ?? ""
}`}
>
{typeof children === "function" ? children({ closeDropdown: close }) : children}
</div>
</div>

View File

@ -15,8 +15,9 @@ import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IUser, IWorkspace, TOnboardingSteps } from "@plane/types";
// ui
import { CustomSelect, Input, Spinner } from "@plane/ui";
import { Input, Spinner } from "@plane/ui";
import { validateWorkspaceName, validateSlug } from "@plane/utils";
import { SelectionDropdown } from "@/components/common/selection-dropdown";
// hooks
import { useWorkspace } from "@/hooks/store/use-workspace";
import { useUserProfile, useUserSettings } from "@/hooks/store/user";
@ -240,25 +241,22 @@ export const CreateWorkspace = observer(function CreateWorkspace(props: Props) {
control={control}
rules={{ required: t("common.errors.required") }}
render={({ field: { value, onChange } }) => (
<CustomSelect
value={value}
onChange={onChange}
label={
<SelectionDropdown
options={ORGANIZATION_SIZE.map((item) => ({
key: item,
title: getOrganizationSizeLabel(item),
isChecked: value === item,
onClick: () => onChange(item),
}))}
menuButton={
(value ? getOrganizationSizeLabel(value) : undefined) ?? (
<span className="text-placeholder">
{t("workspace_creation.form.organization_size.placeholder")}
</span>
)
}
buttonClassName="border border-subtle bg-layer-2 !shadow-none !rounded-md"
input
>
{ORGANIZATION_SIZE.map((item) => (
<CustomSelect.Option key={item} value={item}>
{getOrganizationSizeLabel(item)}
</CustomSelect.Option>
))}
</CustomSelect>
menuButtonWrapperClassName="rounded-md border border-subtle bg-layer-2 px-3 py-2 text-13 shadow-none"
/>
)}
/>
{errors.organization_size && (

View File

@ -17,8 +17,9 @@ import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { Tooltip } from "@plane/propel/tooltip";
import { EFileAssetType } from "@plane/types";
import type { IProject, IWorkspace } from "@plane/types";
import { CustomSelect, Input, TextArea } from "@plane/ui";
import { Input, TextArea } from "@plane/ui";
import { renderFormattedDate } from "@plane/utils";
import { SelectionDropdown } from "@/components/common/selection-dropdown";
import { CoverImage } from "@/components/common/cover-image";
import { ImagePickerPopover } from "@/components/core/image-picker-popover";
import { TimezoneSelect } from "@/components/global";
@ -369,10 +370,22 @@ export function ProjectDetailsForm(props: IProjectDetailsForm) {
render={({ field: { value, onChange } }) => {
const selectedNetwork = NETWORK_CHOICES.find((n) => n.key === value);
return (
<CustomSelect
value={value}
onChange={onChange}
label={
<SelectionDropdown
options={NETWORK_CHOICES.map((network) => ({
key: String(network.key),
title: (
<div className="flex items-start gap-2">
<ProjectNetworkIcon iconKey={network.iconKey} className="h-3.5 w-3.5" />
<div className="-mt-1">
<p>{t(network.i18n_label)}</p>
<p className="text-11 text-placeholder">{t(network.description)}</p>
</div>
</div>
),
isChecked: value === network.key,
onClick: () => onChange(network.key),
}))}
menuButton={
<div className="flex items-center gap-1">
{selectedNetwork ? (
<>
@ -384,23 +397,9 @@ export function ProjectDetailsForm(props: IProjectDetailsForm) {
)}
</div>
}
buttonClassName="nodedc-settings-select !h-12 font-medium"
input
menuButtonWrapperClassName="nodedc-settings-select !h-12 font-medium"
disabled={!isAdmin}
// optionsClassName="w-full"
>
{NETWORK_CHOICES.map((network) => (
<CustomSelect.Option key={network.key} value={network.key}>
<div className="flex items-start gap-2">
<ProjectNetworkIcon iconKey={network.iconKey} className="h-3.5 w-3.5" />
<div className="-mt-1">
<p>{t(network.i18n_label)}</p>
<p className="text-11 text-placeholder">{t(network.description)}</p>
</div>
</div>
</CustomSelect.Option>
))}
</CustomSelect>
/>
);
}}
/>

View File

@ -13,9 +13,10 @@ import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
import { PlusIcon, CloseIcon, ChevronDownIcon } from "@plane/propel/icons";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { Avatar, CustomSelect, EModalPosition, EModalWidth, ModalCore, SearchSelectionDropdown } from "@plane/ui";
import { Avatar, EModalPosition, EModalWidth, ModalCore, SearchSelectionDropdown } from "@plane/ui";
// helpers
import { getFileURL } from "@plane/utils";
import { SelectionDropdown } from "@/components/common/selection-dropdown";
// hooks
import { useMember } from "@/hooks/store/use-member";
import { useUserPermissions } from "@/hooks/store/user";
@ -245,9 +246,20 @@ export const SendProjectInvitationModal = observer(function SendProjectInvitatio
control={control}
rules={{ required: t("project_invitation_modal.select_role_required") }}
render={({ field }) => (
<CustomSelect
{...field}
customButton={
<SelectionDropdown
options={Object.entries(checkCurrentOptionWorkspaceRole(watch(`members.${index}.member_id`)))
.filter(([key]) => parseInt(key) <= (currentProjectRole ?? EUserPermissions.GUEST))
.map(([key, label]) => ({
key,
title: label,
isChecked: String(field.value) === key,
onClick: () =>
setValue(
`members.${index}.role`,
EUserPermissions[ROLE[parseInt(key)].toUpperCase() as keyof typeof EUserPermissions]
),
}))}
menuButton={
<div className="shadow-sm flex w-24 items-center justify-between gap-1 rounded-md border border-subtle px-3 py-2.5 text-left text-13 text-secondary duration-300 hover:bg-layer-1 hover:text-primary focus:outline-none">
<span className="capitalize">
{field.value ? ROLE[field.value] : t("project_invitation_modal.select_role")}
@ -255,20 +267,8 @@ export const SendProjectInvitationModal = observer(function SendProjectInvitatio
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
</div>
}
input
>
{Object.entries(checkCurrentOptionWorkspaceRole(watch(`members.${index}.member_id`))).map(
([key, label]) => {
if (parseInt(key) > (currentProjectRole ?? EUserPermissions.GUEST)) return null;
return (
<CustomSelect.Option key={key} value={key}>
{label}
</CustomSelect.Option>
);
}
)}
</CustomSelect>
menuButtonWrapperClassName="w-24"
/>
)}
/>
{errors.members && errors.members[index]?.role && (

View File

@ -13,8 +13,9 @@ import { Disclosure } from "@headlessui/react";
import { ROLE, EUserPermissions, MEMBER_TRACKER_ELEMENTS } from "@plane/constants";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { EUserProjectRoles, IUser, IWorkspaceMember, TProjectMembership } from "@plane/types";
import { ActionDropdown, CustomSelect } from "@plane/ui";
import { ActionDropdown } from "@plane/ui";
import { getFileURL } from "@plane/utils";
import { SelectionDropdown } from "@/components/common/selection-dropdown";
// hooks
import { useMember } from "@/hooks/store/use-member";
import { useUser, useUserPermissions } from "@/hooks/store/user";
@ -152,39 +153,35 @@ export const AccountTypeColumn = observer(function AccountTypeColumn(props: Acco
control={control}
rules={{ required: "Role is required." }}
render={() => (
<CustomSelect
value={rowData.original_role}
onChange={async (value: EUserProjectRoles) => {
if (!workspaceSlug) return;
await updateMemberRole(workspaceSlug.toString(), projectId.toString(), rowData.member.id, value).catch(
(err) => {
console.log(err, "err");
const error = err.error;
const errorString = Array.isArray(error) ? error[0] : error;
<SelectionDropdown
options={Object.entries(checkCurrentOptionWorkspaceRole(rowData.member.id)).map(([key, label]) => ({
key,
title: label,
isChecked: String(rowData.original_role) === key,
onClick: async () => {
if (!workspaceSlug) return;
await updateMemberRole(workspaceSlug.toString(), projectId.toString(), rowData.member.id, key).catch(
(err) => {
console.log(err, "err");
const error = err.error;
const errorString = Array.isArray(error) ? error[0] : error;
setToast({
type: TOAST_TYPE.ERROR,
title: "You cant change this role yet.",
message: errorString ?? "An error occurred while updating member role. Please try again.",
});
}
);
}}
label={
setToast({
type: TOAST_TYPE.ERROR,
title: "You cant change this role yet.",
message: errorString ?? "An error occurred while updating member role. Please try again.",
});
}
);
},
}))}
menuButton={
<div className="flex">
<span>{roleLabel}</span>
</div>
}
buttonClassName={`!px-0 !justify-start hover:bg-surface-1 ${errors.role ? "border-danger-strong" : "border-none"}`}
className="w-32 rounded-md p-0"
input
>
{Object.entries(checkCurrentOptionWorkspaceRole(rowData.member.id)).map(([key, label]) => (
<CustomSelect.Option key={key} value={key}>
{label}
</CustomSelect.Option>
))}
</CustomSelect>
menuButtonWrapperClassName={`w-32 rounded-md p-0 !justify-start !px-0 hover:bg-surface-1 ${errors.role ? "border-danger-strong" : "border-none"}`}
/>
)}
/>
) : (

View File

@ -8,7 +8,7 @@ import { observer } from "mobx-react";
// plane imports
import { SUPPORTED_LANGUAGES, useTranslation } from "@plane/i18n";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { CustomSelect } from "@plane/ui";
import { SelectionDropdown } from "@/components/common/selection-dropdown";
// components
import { TimezoneSelect } from "@/components/global";
import { StartOfWeekPreference } from "@/components/profile/start-of-week-preference";
@ -79,21 +79,17 @@ export const ProfileSettingsLanguageAndTimezonePreferencesList = observer(
title={t("language")}
description={t("language_setting")}
control={
<CustomSelect
value={profile?.language}
label={profile?.language ? getLanguageLabel(profile?.language) : "Select a language"}
onChange={handleLanguageChange}
buttonClassName="border border-subtle-1"
className="rounded-md"
input
<SelectionDropdown
options={SUPPORTED_LANGUAGES.map((item) => ({
key: item.value,
title: item.label,
isChecked: profile?.language === item.value,
onClick: () => handleLanguageChange(item.value),
}))}
menuButton={profile?.language ? getLanguageLabel(profile?.language) : "Select a language"}
menuButtonWrapperClassName="rounded-md border border-subtle-1 px-3 py-2 text-13"
placement="bottom-end"
>
{SUPPORTED_LANGUAGES.map((item) => (
<CustomSelect.Option key={item.value} value={item.value}>
{item.label}
</CustomSelect.Option>
))}
</CustomSelect>
/>
}
/>
<StartOfWeekPreference

View File

@ -14,8 +14,9 @@ import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IWorkspace } from "@plane/types";
// ui
import { CustomSelect, Input } from "@plane/ui";
import { Input } from "@plane/ui";
import { validateWorkspaceName, validateSlug } from "@plane/utils";
import { SelectionDropdown } from "@/components/common/selection-dropdown";
// hooks
import { useWorkspace } from "@/hooks/store/use-workspace";
import { useAppRouter } from "@/hooks/use-app-router";
@ -212,25 +213,22 @@ export const CreateWorkspaceForm = observer(function CreateWorkspaceForm(props:
control={control}
rules={{ required: t("common.errors.required") }}
render={({ field: { value, onChange } }) => (
<CustomSelect
value={value}
onChange={onChange}
label={
<SelectionDropdown
options={ORGANIZATION_SIZE.map((item) => ({
key: item,
title: item,
isChecked: value === item,
onClick: () => onChange(item),
}))}
menuButton={
ORGANIZATION_SIZE.find((c) => c === value) ?? (
<span className="text-placeholder">
{t("workspace_creation.form.organization_size.placeholder")}
</span>
)
}
buttonClassName="border border-subtle bg-layer-2 !shadow-none !rounded-md"
input
>
{ORGANIZATION_SIZE.map((item) => (
<CustomSelect.Option key={item} value={item}>
{item}
</CustomSelect.Option>
))}
</CustomSelect>
menuButtonWrapperClassName="rounded-md border border-subtle bg-layer-2 px-3 py-2 text-13 shadow-none"
/>
)}
/>
{errors.organization_size && (

View File

@ -11,7 +11,8 @@ import { Controller } from "react-hook-form";
import { ROLE } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { CloseIcon } from "@plane/propel/icons";
import { CustomSelect, Input } from "@plane/ui";
import { Input } from "@plane/ui";
import { SelectionDropdown } from "@/components/common/selection-dropdown";
import { cn } from "@plane/utils";
// hooks
import { useUserPermissions } from "@/hooks/store/user";
@ -89,22 +90,18 @@ export const InvitationFields = observer(function InvitationFields(props: TInvit
name={`emails.${index}.role`}
rules={{ required: true }}
render={({ field: { value, onChange } }) => (
<CustomSelect
value={value}
label={<span className="text-caption-sm-regular sm:text-body-xs-regular">{ROLE[value]}</span>}
onChange={onChange}
className="w-24 flex-grow"
input
>
{Object.entries(ROLE).map(([key, value]) => {
if (currentWorkspaceRole && currentWorkspaceRole >= parseInt(key))
return (
<CustomSelect.Option key={key} value={parseInt(key)}>
{value}
</CustomSelect.Option>
);
})}
</CustomSelect>
<SelectionDropdown
options={Object.entries(ROLE)
.filter(([key]) => Boolean(currentWorkspaceRole && currentWorkspaceRole >= parseInt(key)))
.map(([key, roleValue]) => ({
key,
title: roleValue,
isChecked: value === parseInt(key),
onClick: () => onChange(parseInt(key)),
}))}
menuButton={<span className="text-caption-sm-regular sm:text-body-xs-regular">{ROLE[value]}</span>}
menuButtonWrapperClassName="w-24 flex-grow px-3 py-2 text-13"
/>
)}
/>
</div>

View File

@ -13,8 +13,9 @@ import { useTranslation } from "@plane/i18n";
import { LinkIcon, TrashIcon, ChevronDownIcon } from "@plane/propel/icons";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { TContextMenuItem } from "@plane/ui";
import { ActionDropdown, CustomSelect } from "@plane/ui";
import { ActionDropdown } from "@plane/ui";
import { copyTextToClipboard } from "@plane/utils";
import { SelectionDropdown } from "@/components/common/selection-dropdown";
// components
import { ConfirmWorkspaceMemberRemove } from "@/components/workspace/confirm-workspace-member-remove";
// hooks
@ -134,8 +135,38 @@ export const WorkspaceInvitationsListItem = observer(function WorkspaceInvitatio
<div className="flex items-center justify-center rounded-sm bg-label-yellow-bg-strong/20 px-2.5 py-1 text-center text-caption-sm-medium text-label-yellow-text">
<p>{t("common.pending")}</p>
</div>
<CustomSelect
customButton={
<SelectionDropdown
options={Object.keys(ROLE)
.filter((key) => {
if (
currentWorkspaceRole &&
Number(currentWorkspaceRole) !== 20 &&
Number(currentWorkspaceRole) < parseInt(key)
)
return false;
return true;
})
.map((key) => ({
key,
title: ROLE[parseInt(key) as keyof typeof ROLE],
isChecked: invitationDetails.role === parseInt(key, 10),
onClick: () => {
if (!workspaceSlug) return;
updateMemberInvitation(workspaceSlug.toString(), invitationDetails.id, {
role: parseInt(key, 10),
}).catch((err: unknown) => {
const error = err as { error?: string };
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: error?.error || "An error occurred while updating member role. Please try again.",
});
});
},
}))}
menuButton={
<div className="item-center flex gap-1 rounded-sm px-2 py-0.5">
<span
className={`flex items-center rounded-sm text-caption-sm-medium ${
@ -151,39 +182,9 @@ export const WorkspaceInvitationsListItem = observer(function WorkspaceInvitatio
)}
</div>
}
value={invitationDetails.role}
onChange={(value: EUserPermissions) => {
if (!workspaceSlug || !value) return;
updateMemberInvitation(workspaceSlug.toString(), invitationDetails.id, {
role: value,
}).catch((err: unknown) => {
const error = err as { error?: string };
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: error?.error || "An error occurred while updating member role. Please try again.",
});
});
}}
disabled={!hasRoleChangeAccess}
placement="bottom-end"
>
{Object.keys(ROLE).map((key) => {
if (
currentWorkspaceRole &&
Number(currentWorkspaceRole) !== 20 &&
Number(currentWorkspaceRole) < parseInt(key)
)
return null;
return (
<CustomSelect.Option key={key} value={parseInt(key, 10)}>
<>{ROLE[parseInt(key) as keyof typeof ROLE]}</>
</CustomSelect.Option>
);
})}
</CustomSelect>
/>
{isAdmin && (
<ActionDropdown placement="bottom-end" items={MENU_ITEMS} />
)}

View File

@ -16,9 +16,10 @@ import { Pill, EPillVariant, EPillSize } from "@plane/propel/pill";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IUser, IWorkspaceMember } from "@plane/types";
// plane ui
import { CustomSelect, PopoverMenu } from "@plane/ui";
import { PopoverMenu } from "@plane/ui";
// helpers
import { getFileURL } from "@plane/utils";
import { SelectionDropdown } from "@/components/common/selection-dropdown";
// hooks
import { useMember } from "@/hooks/store/use-member";
import { useUser, useUserPermissions } from "@/hooks/store/user";
@ -151,40 +152,36 @@ export const AccountTypeColumn = observer(function AccountTypeColumn(props: Acco
control={control}
rules={{ required: "Role is required." }}
render={({ field: { value } }) => (
<CustomSelect
value={value as EUserPermissions}
onChange={async (value: EUserPermissions) => {
if (!workspaceSlug) return;
try {
await updateMember(workspaceSlug.toString(), rowData.member.id, {
role: value as unknown as EUserPermissions,
});
} catch (err: unknown) {
const error = err as { error?: string | string[] };
const errorString = Array.isArray(error?.error) ? error.error[0] : error?.error;
<SelectionDropdown
options={Object.keys(ROLE).map((item) => ({
key: item,
title: ROLE[item as unknown as keyof typeof ROLE],
isChecked: String(value) === item,
onClick: async () => {
if (!workspaceSlug) return;
try {
await updateMember(workspaceSlug.toString(), rowData.member.id, {
role: item as unknown as EUserPermissions,
});
} catch (err: unknown) {
const error = err as { error?: string | string[] };
const errorString = Array.isArray(error?.error) ? error.error[0] : error?.error;
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: errorString ?? "An error occurred while updating member role. Please try again.",
});
}
}}
label={
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: errorString ?? "An error occurred while updating member role. Please try again.",
});
}
},
}))}
menuButton={
<div className="flex">
<span>{ROLE[rowData.role]}</span>
</div>
}
buttonClassName={`!px-0 !justify-start hover:bg-surface-1 ${errors.role ? "border-danger-strong" : "border-none"}`}
className="w-32 rounded-md p-0"
input
>
{Object.keys(ROLE).map((item) => (
<CustomSelect.Option key={item} value={item as unknown as EUserPermissions}>
{ROLE[item as unknown as keyof typeof ROLE]}
</CustomSelect.Option>
))}
</CustomSelect>
menuButtonWrapperClassName={`w-32 rounded-md p-0 !justify-start !px-0 hover:bg-surface-1 ${errors.role ? "border-danger-strong" : "border-none"}`}
/>
)}
/>
)}

View File

@ -14,8 +14,9 @@ import { Button } from "@plane/propel/button";
import { EditIcon } from "@plane/propel/icons";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IWorkspace } from "@plane/types";
import { CustomSelect, Input } from "@plane/ui";
import { Input } from "@plane/ui";
import { cn, copyUrlToClipboard, getFileURL, validateWorkspaceName } from "@plane/utils";
import { SelectionDropdown } from "@/components/common/selection-dropdown";
// components
import { WorkspaceImageUploadModal } from "@/components/core/modals/workspace-image-upload-modal";
import { TimezoneSelect } from "@/components/global/timezone-select";
@ -222,23 +223,20 @@ export const WorkspaceDetails = observer(function WorkspaceDetails() {
name="organization_size"
control={control}
render={({ field: { value, onChange } }) => (
<CustomSelect
value={value}
onChange={onChange}
label={
<SelectionDropdown
options={ORGANIZATION_SIZE.map((item) => ({
key: item,
title: item,
isChecked: value === item,
onClick: () => onChange(item),
}))}
menuButton={
ORGANIZATION_SIZE.find((c) => c === value) ??
t("workspace_settings.settings.general.errors.company_size.select_a_range")
}
buttonClassName="border border-subtle bg-layer-2 !shadow-none !rounded-md"
input
menuButtonWrapperClassName="rounded-md border border-subtle bg-layer-2 px-3 py-2 text-13 shadow-none"
disabled={!isAdmin}
>
{ORGANIZATION_SIZE.map((item) => (
<CustomSelect.Option key={item} value={item}>
{item}
</CustomSelect.Option>
))}
</CustomSelect>
/>
)}
/>
</div>