250 lines
6.2 KiB
TypeScript
250 lines
6.2 KiB
TypeScript
|
|
import React, { useState } from "react";
|
||
|
|
import { find, noop } from "lodash";
|
||
|
|
import { DropdownOption } from "widgets/DropdownWidget";
|
||
|
|
import {
|
||
|
|
PopoverInteractionKind,
|
||
|
|
PopoverPosition,
|
||
|
|
IPopoverSharedProps,
|
||
|
|
MenuItem,
|
||
|
|
Popover,
|
||
|
|
Menu,
|
||
|
|
} from "@blueprintjs/core";
|
||
|
|
import styled from "styled-components";
|
||
|
|
import Icon, { IconSize } from "./Icon";
|
||
|
|
|
||
|
|
export type TreeDropdownOption = DropdownOption & {
|
||
|
|
onSelect?: (value: TreeDropdownOption, setter?: Setter) => void;
|
||
|
|
children?: TreeDropdownOption[];
|
||
|
|
className?: string;
|
||
|
|
type?: string;
|
||
|
|
};
|
||
|
|
|
||
|
|
type Setter = (value: TreeDropdownOption, defaultVal?: string) => void;
|
||
|
|
|
||
|
|
type TreeDropdownProps = {
|
||
|
|
optionTree: TreeDropdownOption[];
|
||
|
|
selectedValue: string;
|
||
|
|
getDefaults?: (value: any) => any;
|
||
|
|
defaultText: string;
|
||
|
|
onSelect: Setter;
|
||
|
|
selectedLabelModifier?: (
|
||
|
|
option: TreeDropdownOption,
|
||
|
|
displayValue?: string,
|
||
|
|
) => React.ReactNode;
|
||
|
|
displayValue?: string;
|
||
|
|
toggle?: React.ReactNode;
|
||
|
|
className?: string;
|
||
|
|
modifiers?: IPopoverSharedProps["modifiers"];
|
||
|
|
};
|
||
|
|
|
||
|
|
const MoreActionableContainer = styled.div<{ isOpen: boolean }>`
|
||
|
|
width: 34px;
|
||
|
|
height: 30px;
|
||
|
|
cursor: pointer;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
background-color: transparent;
|
||
|
|
|
||
|
|
&&&& span {
|
||
|
|
width: auto;
|
||
|
|
}
|
||
|
|
|
||
|
|
&&&& svg > path {
|
||
|
|
fill: ${(props) =>
|
||
|
|
props.theme.colors.apiPane.moreActions.targetIcon.normal};
|
||
|
|
}
|
||
|
|
|
||
|
|
${(props) =>
|
||
|
|
props.isOpen
|
||
|
|
? `
|
||
|
|
background-color: ${props.theme.colors.apiPane.moreActions.targetBg};
|
||
|
|
|
||
|
|
&&&& svg > path {
|
||
|
|
fill: ${props.theme.colors.apiPane.moreActions.targetIcon.hover};
|
||
|
|
}
|
||
|
|
`
|
||
|
|
: null}
|
||
|
|
|
||
|
|
&:hover {
|
||
|
|
background-color: ${(props) =>
|
||
|
|
props.theme.colors.apiPane.moreActions.targetBg};
|
||
|
|
|
||
|
|
&&&& svg > path {
|
||
|
|
fill: ${(props) =>
|
||
|
|
props.theme.colors.apiPane.moreActions.targetIcon.hover};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
`;
|
||
|
|
|
||
|
|
const StyledPopover = styled.div`
|
||
|
|
.bp3-transition-container {
|
||
|
|
top: 4px !important;
|
||
|
|
}
|
||
|
|
|
||
|
|
.bp3-popover {
|
||
|
|
border-radius: 0px;
|
||
|
|
box-shadow: ${(props) => props.theme.colors.apiPane.moreActions.menuShadow};
|
||
|
|
|
||
|
|
.bp3-popover-content {
|
||
|
|
border-radius: 0px;
|
||
|
|
}
|
||
|
|
|
||
|
|
&&& ul {
|
||
|
|
background-color: ${(props) =>
|
||
|
|
props.theme.colors.apiPane.moreActions.menuBg.normal};
|
||
|
|
}
|
||
|
|
|
||
|
|
.bp3-menu {
|
||
|
|
min-width: 220px;
|
||
|
|
padding: 0px;
|
||
|
|
border-radius: 0px;
|
||
|
|
background-color: ${(props) =>
|
||
|
|
props.theme.colors.apiPane.moreActions.menuBg.normal};
|
||
|
|
|
||
|
|
.bp3-menu-item {
|
||
|
|
font-size: 14px;
|
||
|
|
line-height: 20px;
|
||
|
|
letter-spacing: -0.24px;
|
||
|
|
padding: 10px 15px;
|
||
|
|
color: ${(props) =>
|
||
|
|
props.theme.colors.apiPane.moreActions.menuText.normal};
|
||
|
|
.bp3-icon > svg:not([fill]) {
|
||
|
|
fill: #9f9f9f;
|
||
|
|
}
|
||
|
|
|
||
|
|
&:active,
|
||
|
|
&:hover {
|
||
|
|
background-color: ${(props) =>
|
||
|
|
props.theme.colors.apiPane.moreActions.menuBg.hover};
|
||
|
|
color: ${(props) =>
|
||
|
|
props.theme.colors.apiPane.moreActions.menuText.hover};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
.bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-menu-item {
|
||
|
|
background-color: ${(props) =>
|
||
|
|
props.theme.colors.apiPane.moreActions.menuBg.hover};
|
||
|
|
color: ${(props) =>
|
||
|
|
props.theme.colors.apiPane.moreActions.menuText.hover};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
`;
|
||
|
|
|
||
|
|
function getSelectedOption(
|
||
|
|
selectedValue: string,
|
||
|
|
defaultText: string,
|
||
|
|
options: TreeDropdownOption[],
|
||
|
|
) {
|
||
|
|
let selectedOption: TreeDropdownOption = {
|
||
|
|
label: defaultText,
|
||
|
|
value: "",
|
||
|
|
};
|
||
|
|
options.length > 0 &&
|
||
|
|
options.forEach((option) => {
|
||
|
|
// Find the selected option in the OptionsTree
|
||
|
|
if (option.value === selectedValue) {
|
||
|
|
selectedOption = option;
|
||
|
|
} else {
|
||
|
|
const childOption = find(option.children, {
|
||
|
|
value: selectedValue,
|
||
|
|
});
|
||
|
|
if (childOption) {
|
||
|
|
selectedOption = childOption;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
return selectedOption;
|
||
|
|
}
|
||
|
|
|
||
|
|
export default function TreeDropdown(props: TreeDropdownProps) {
|
||
|
|
const {
|
||
|
|
selectedValue,
|
||
|
|
defaultText,
|
||
|
|
optionTree,
|
||
|
|
onSelect,
|
||
|
|
getDefaults,
|
||
|
|
} = props;
|
||
|
|
const selectedOption = getSelectedOption(
|
||
|
|
selectedValue,
|
||
|
|
defaultText,
|
||
|
|
optionTree,
|
||
|
|
);
|
||
|
|
|
||
|
|
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||
|
|
|
||
|
|
const handleSelect = (option: TreeDropdownOption) => {
|
||
|
|
if (option.onSelect) {
|
||
|
|
option.onSelect(option, props.onSelect);
|
||
|
|
} else {
|
||
|
|
const defaultVal = getDefaults ? getDefaults(option.value) : undefined;
|
||
|
|
onSelect(option, defaultVal);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
function renderTreeOption(option: TreeDropdownOption) {
|
||
|
|
const isSelected =
|
||
|
|
selectedOption.value === option.value ||
|
||
|
|
selectedOption.type === option.value;
|
||
|
|
return (
|
||
|
|
<MenuItem
|
||
|
|
className={option.className || "single-select"}
|
||
|
|
active={isSelected}
|
||
|
|
key={option.value}
|
||
|
|
icon={option.id === "create" ? "plus" : undefined}
|
||
|
|
onClick={
|
||
|
|
option.children
|
||
|
|
? noop
|
||
|
|
: (e: any) => {
|
||
|
|
handleSelect(option);
|
||
|
|
setIsOpen(false);
|
||
|
|
e.stopPropagation();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
text={option.label}
|
||
|
|
intent={option.intent}
|
||
|
|
popoverProps={{
|
||
|
|
minimal: true,
|
||
|
|
interactionKind: PopoverInteractionKind.CLICK,
|
||
|
|
position: PopoverPosition.LEFT,
|
||
|
|
targetProps: { onClick: (e: any) => e.stopPropagation() },
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{option.children && option.children.map(renderTreeOption)}
|
||
|
|
</MenuItem>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
const list = optionTree.map(renderTreeOption);
|
||
|
|
const menuItems = <Menu>{list}</Menu>;
|
||
|
|
const defaultToggle = (
|
||
|
|
<MoreActionableContainer isOpen={isOpen} className={props.className}>
|
||
|
|
<Icon name="context-menu" size={IconSize.XXXL} />
|
||
|
|
</MoreActionableContainer>
|
||
|
|
);
|
||
|
|
return (
|
||
|
|
<StyledPopover>
|
||
|
|
<Popover
|
||
|
|
usePortal={false}
|
||
|
|
isOpen={isOpen}
|
||
|
|
minimal
|
||
|
|
content={menuItems}
|
||
|
|
position={PopoverPosition.LEFT}
|
||
|
|
className="wrapper-popover"
|
||
|
|
modifiers={props.modifiers}
|
||
|
|
onClose={() => {
|
||
|
|
setIsOpen(false);
|
||
|
|
}}
|
||
|
|
targetProps={{
|
||
|
|
onClick: (e: any) => {
|
||
|
|
setIsOpen(true);
|
||
|
|
e.stopPropagation();
|
||
|
|
},
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{defaultToggle}
|
||
|
|
</Popover>
|
||
|
|
</StyledPopover>
|
||
|
|
);
|
||
|
|
}
|