АРХ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: миграция form и settings select-пикеров на SelectionDropdown
This commit is contained in:
parent
8fa5de24eb
commit
ecb31a78f9
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 && (
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -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 && (
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 && (
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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} />
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue