АРХ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: миграция 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 { NETWORK_CHOICES, ETabIndices } from "@plane/constants";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import type { IProject } from "@plane/types"; import type { IProject } from "@plane/types";
import { CustomSelect } from "@plane/ui"; import { SelectionDropdown } from "@/components/common/selection-dropdown";
import { getTabIndex } from "@plane/utils"; import { getTabIndex } from "@plane/utils";
// components // components
import { MemberDropdown } from "@/components/dropdowns/member/dropdown"; import { MemberDropdown } from "@/components/dropdowns/member/dropdown";
@ -36,10 +36,22 @@ function ProjectAttributes(props: Props) {
return ( return (
<div className="h-7 flex-shrink-0" tabIndex={getIndex("network")}> <div className="h-7 flex-shrink-0" tabIndex={getIndex("network")}>
<CustomSelect <SelectionDropdown
value={value} options={NETWORK_CHOICES.map((network) => ({
onChange={onChange} key: String(network.key),
label={ 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"> <div className="flex h-full items-center gap-1">
{currentNetwork ? ( {currentNetwork ? (
<> <>
@ -52,23 +64,9 @@ function ProjectAttributes(props: Props) {
</div> </div>
} }
placement="bottom-start" placement="bottom-start"
className="h-10" menuButtonWrapperClassName={projectAttributeChipClassName}
buttonClassName={projectAttributeChipClassName}
noChevron
tabIndex={getIndex("network")} 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> </div>
); );
}} }}

View File

@ -21,6 +21,10 @@ export type TSelectionDropdownOption = {
type Props = { type Props = {
disabled?: boolean; disabled?: boolean;
dropdownClassName?: string;
dropdownContentClassName?: string;
footerContent?: ReactNode;
headerContent?: ReactNode;
menuButton: ReactNode | ((props: { open: boolean }) => ReactNode); menuButton: ReactNode | ((props: { open: boolean }) => ReactNode);
menuButtonWrapperClassName?: string; menuButtonWrapperClassName?: string;
options: TSelectionDropdownOption[]; options: TSelectionDropdownOption[];
@ -30,7 +34,19 @@ type Props = {
}; };
export function SelectionDropdown(props: 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); const renderedOptions = options.filter((option) => option.shouldRender !== false);
@ -41,9 +57,12 @@ export function SelectionDropdown(props: Props) {
placement={placement} placement={placement}
disabled={disabled} disabled={disabled}
tabIndex={tabIndex} tabIndex={tabIndex}
dropdownClassName={dropdownClassName}
dropdownContentClassName={dropdownContentClassName}
> >
{({ closeDropdown }) => ( {({ closeDropdown }) => (
<div className="vertical-scrollbar relative scrollbar-sm h-full w-full overflow-y-auto px-2.5 py-2"> <div className="vertical-scrollbar relative scrollbar-sm h-full w-full overflow-y-auto px-2.5 py-2">
{headerContent}
{title && ( {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"> <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} {title}
@ -65,6 +84,7 @@ export function SelectionDropdown(props: Props) {
/> />
))} ))}
</div> </div>
{footerContent}
</div> </div>
)} )}
</FiltersDropdown> </FiltersDropdown>

View File

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

View File

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

View File

@ -17,8 +17,9 @@ import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { Tooltip } from "@plane/propel/tooltip"; import { Tooltip } from "@plane/propel/tooltip";
import { EFileAssetType } from "@plane/types"; import { EFileAssetType } from "@plane/types";
import type { IProject, IWorkspace } 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 { renderFormattedDate } from "@plane/utils";
import { SelectionDropdown } from "@/components/common/selection-dropdown";
import { CoverImage } from "@/components/common/cover-image"; import { CoverImage } from "@/components/common/cover-image";
import { ImagePickerPopover } from "@/components/core/image-picker-popover"; import { ImagePickerPopover } from "@/components/core/image-picker-popover";
import { TimezoneSelect } from "@/components/global"; import { TimezoneSelect } from "@/components/global";
@ -369,10 +370,22 @@ export function ProjectDetailsForm(props: IProjectDetailsForm) {
render={({ field: { value, onChange } }) => { render={({ field: { value, onChange } }) => {
const selectedNetwork = NETWORK_CHOICES.find((n) => n.key === value); const selectedNetwork = NETWORK_CHOICES.find((n) => n.key === value);
return ( return (
<CustomSelect <SelectionDropdown
value={value} options={NETWORK_CHOICES.map((network) => ({
onChange={onChange} key: String(network.key),
label={ 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"> <div className="flex items-center gap-1">
{selectedNetwork ? ( {selectedNetwork ? (
<> <>
@ -384,23 +397,9 @@ export function ProjectDetailsForm(props: IProjectDetailsForm) {
)} )}
</div> </div>
} }
buttonClassName="nodedc-settings-select !h-12 font-medium" menuButtonWrapperClassName="nodedc-settings-select !h-12 font-medium"
input
disabled={!isAdmin} 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 { Button } from "@plane/propel/button";
import { PlusIcon, CloseIcon, ChevronDownIcon } from "@plane/propel/icons"; import { PlusIcon, CloseIcon, ChevronDownIcon } from "@plane/propel/icons";
import { TOAST_TYPE, setToast } from "@plane/propel/toast"; 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 // helpers
import { getFileURL } from "@plane/utils"; import { getFileURL } from "@plane/utils";
import { SelectionDropdown } from "@/components/common/selection-dropdown";
// hooks // hooks
import { useMember } from "@/hooks/store/use-member"; import { useMember } from "@/hooks/store/use-member";
import { useUserPermissions } from "@/hooks/store/user"; import { useUserPermissions } from "@/hooks/store/user";
@ -245,9 +246,20 @@ export const SendProjectInvitationModal = observer(function SendProjectInvitatio
control={control} control={control}
rules={{ required: t("project_invitation_modal.select_role_required") }} rules={{ required: t("project_invitation_modal.select_role_required") }}
render={({ field }) => ( render={({ field }) => (
<CustomSelect <SelectionDropdown
{...field} options={Object.entries(checkCurrentOptionWorkspaceRole(watch(`members.${index}.member_id`)))
customButton={ .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"> <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"> <span className="capitalize">
{field.value ? ROLE[field.value] : t("project_invitation_modal.select_role")} {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" /> <ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
</div> </div>
} }
input menuButtonWrapperClassName="w-24"
> />
{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>
)} )}
/> />
{errors.members && errors.members[index]?.role && ( {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 { ROLE, EUserPermissions, MEMBER_TRACKER_ELEMENTS } from "@plane/constants";
import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { EUserProjectRoles, IUser, IWorkspaceMember, TProjectMembership } from "@plane/types"; 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 { getFileURL } from "@plane/utils";
import { SelectionDropdown } from "@/components/common/selection-dropdown";
// hooks // hooks
import { useMember } from "@/hooks/store/use-member"; import { useMember } from "@/hooks/store/use-member";
import { useUser, useUserPermissions } from "@/hooks/store/user"; import { useUser, useUserPermissions } from "@/hooks/store/user";
@ -152,11 +153,14 @@ export const AccountTypeColumn = observer(function AccountTypeColumn(props: Acco
control={control} control={control}
rules={{ required: "Role is required." }} rules={{ required: "Role is required." }}
render={() => ( render={() => (
<CustomSelect <SelectionDropdown
value={rowData.original_role} options={Object.entries(checkCurrentOptionWorkspaceRole(rowData.member.id)).map(([key, label]) => ({
onChange={async (value: EUserProjectRoles) => { key,
title: label,
isChecked: String(rowData.original_role) === key,
onClick: async () => {
if (!workspaceSlug) return; if (!workspaceSlug) return;
await updateMemberRole(workspaceSlug.toString(), projectId.toString(), rowData.member.id, value).catch( await updateMemberRole(workspaceSlug.toString(), projectId.toString(), rowData.member.id, key).catch(
(err) => { (err) => {
console.log(err, "err"); console.log(err, "err");
const error = err.error; const error = err.error;
@ -169,22 +173,15 @@ export const AccountTypeColumn = observer(function AccountTypeColumn(props: Acco
}); });
} }
); );
}} },
label={ }))}
menuButton={
<div className="flex"> <div className="flex">
<span>{roleLabel}</span> <span>{roleLabel}</span>
</div> </div>
} }
buttonClassName={`!px-0 !justify-start hover:bg-surface-1 ${errors.role ? "border-danger-strong" : "border-none"}`} menuButtonWrapperClassName={`w-32 rounded-md p-0 !justify-start !px-0 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>
)} )}
/> />
) : ( ) : (

View File

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

View File

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

View File

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

View File

@ -13,8 +13,9 @@ import { useTranslation } from "@plane/i18n";
import { LinkIcon, TrashIcon, ChevronDownIcon } from "@plane/propel/icons"; import { LinkIcon, TrashIcon, ChevronDownIcon } from "@plane/propel/icons";
import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { TContextMenuItem } from "@plane/ui"; import type { TContextMenuItem } from "@plane/ui";
import { ActionDropdown, CustomSelect } from "@plane/ui"; import { ActionDropdown } from "@plane/ui";
import { copyTextToClipboard } from "@plane/utils"; import { copyTextToClipboard } from "@plane/utils";
import { SelectionDropdown } from "@/components/common/selection-dropdown";
// components // components
import { ConfirmWorkspaceMemberRemove } from "@/components/workspace/confirm-workspace-member-remove"; import { ConfirmWorkspaceMemberRemove } from "@/components/workspace/confirm-workspace-member-remove";
// hooks // 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"> <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> <p>{t("common.pending")}</p>
</div> </div>
<CustomSelect <SelectionDropdown
customButton={ 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"> <div className="item-center flex gap-1 rounded-sm px-2 py-0.5">
<span <span
className={`flex items-center rounded-sm text-caption-sm-medium ${ className={`flex items-center rounded-sm text-caption-sm-medium ${
@ -151,39 +182,9 @@ export const WorkspaceInvitationsListItem = observer(function WorkspaceInvitatio
)} )}
</div> </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} disabled={!hasRoleChangeAccess}
placement="bottom-end" 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 && ( {isAdmin && (
<ActionDropdown placement="bottom-end" items={MENU_ITEMS} /> <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 { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IUser, IWorkspaceMember } from "@plane/types"; import type { IUser, IWorkspaceMember } from "@plane/types";
// plane ui // plane ui
import { CustomSelect, PopoverMenu } from "@plane/ui"; import { PopoverMenu } from "@plane/ui";
// helpers // helpers
import { getFileURL } from "@plane/utils"; import { getFileURL } from "@plane/utils";
import { SelectionDropdown } from "@/components/common/selection-dropdown";
// hooks // hooks
import { useMember } from "@/hooks/store/use-member"; import { useMember } from "@/hooks/store/use-member";
import { useUser, useUserPermissions } from "@/hooks/store/user"; import { useUser, useUserPermissions } from "@/hooks/store/user";
@ -151,13 +152,16 @@ export const AccountTypeColumn = observer(function AccountTypeColumn(props: Acco
control={control} control={control}
rules={{ required: "Role is required." }} rules={{ required: "Role is required." }}
render={({ field: { value } }) => ( render={({ field: { value } }) => (
<CustomSelect <SelectionDropdown
value={value as EUserPermissions} options={Object.keys(ROLE).map((item) => ({
onChange={async (value: EUserPermissions) => { key: item,
title: ROLE[item as unknown as keyof typeof ROLE],
isChecked: String(value) === item,
onClick: async () => {
if (!workspaceSlug) return; if (!workspaceSlug) return;
try { try {
await updateMember(workspaceSlug.toString(), rowData.member.id, { await updateMember(workspaceSlug.toString(), rowData.member.id, {
role: value as unknown as EUserPermissions, role: item as unknown as EUserPermissions,
}); });
} catch (err: unknown) { } catch (err: unknown) {
const error = err as { error?: string | string[] }; const error = err as { error?: string | string[] };
@ -169,22 +173,15 @@ export const AccountTypeColumn = observer(function AccountTypeColumn(props: Acco
message: errorString ?? "An error occurred while updating member role. Please try again.", message: errorString ?? "An error occurred while updating member role. Please try again.",
}); });
} }
}} },
label={ }))}
menuButton={
<div className="flex"> <div className="flex">
<span>{ROLE[rowData.role]}</span> <span>{ROLE[rowData.role]}</span>
</div> </div>
} }
buttonClassName={`!px-0 !justify-start hover:bg-surface-1 ${errors.role ? "border-danger-strong" : "border-none"}`} menuButtonWrapperClassName={`w-32 rounded-md p-0 !justify-start !px-0 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>
)} )}
/> />
)} )}

View File

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