fleet/frontend/components/TeamsDropdown/TeamsDropdown.tsx
2025-09-29 12:10:41 -05:00

310 lines
8 KiB
TypeScript

import React, { useMemo } from "react";
import Select, {
components,
DropdownIndicatorProps,
GroupBase,
OptionProps,
StylesConfig,
} from "react-select-5";
import { COLORS } from "styles/var/colors";
import { PADDING } from "styles/var/padding";
import { FONT_SIZES, FONT_WEIGHTS } from "styles/var/fonts";
import classnames from "classnames";
import { IDropdownOption } from "interfaces/dropdownOption";
import {
APP_CONTEXT_ALL_TEAMS_SUMMARY,
ITeamSummary,
APP_CONTEXT_NO_TEAM_SUMMARY,
} from "interfaces/team";
import Icon from "components/Icon";
export interface INumberDropdownOption extends Omit<IDropdownOption, "value"> {
value: number; // Redefine the value property to be just number
}
const generateDropdownOptions = (
teams: ITeamSummary[] | undefined,
includeAllTeams: boolean,
includeNoTeams?: boolean
): INumberDropdownOption[] => {
if (!teams) {
return [];
}
const options: INumberDropdownOption[] = teams.map((team) => ({
disabled: false,
label: team.name,
value: team.id,
}));
const filtered = options.filter(
(o) =>
!(
(o.label === APP_CONTEXT_NO_TEAM_SUMMARY.name && !includeNoTeams) ||
(o.label === APP_CONTEXT_ALL_TEAMS_SUMMARY.name && !includeAllTeams)
)
);
return filtered;
};
const getOptionBackgroundColor = (
state: OptionProps<
INumberDropdownOption,
false,
GroupBase<INumberDropdownOption>
>
) => {
return state.isFocused ? COLORS["ui-fleet-black-5"] : "transparent";
};
interface ITeamsDropdownProps {
currentUserTeams: ITeamSummary[];
selectedTeamId?: number;
includeAllTeams?: boolean;
includeNoTeams?: boolean;
isDisabled?: boolean;
onChange: (newSelectedValue: number) => void;
onOpen?: () => void;
onClose?: () => void;
/** Indicates that this teams dropdown should be styled as a form field */
asFormField?: boolean;
}
const baseClass = "team-dropdown";
const TeamsDropdown = ({
currentUserTeams,
selectedTeamId,
includeAllTeams = true,
includeNoTeams = false,
isDisabled = false,
onChange,
onOpen,
onClose,
asFormField = false,
}: ITeamsDropdownProps): JSX.Element => {
const teamOptions: INumberDropdownOption[] = useMemo(
() =>
generateDropdownOptions(
currentUserTeams,
includeAllTeams,
includeNoTeams
),
[currentUserTeams, includeAllTeams, includeNoTeams]
);
const selectedValue = teamOptions.find(
(option) => selectedTeamId === option.value
)
? selectedTeamId
: teamOptions[0]?.value;
const dropdownWrapperClasses = classnames(`${baseClass}-wrapper`, {
disabled: isDisabled || undefined,
});
const CustomDropdownIndicator = (
props: DropdownIndicatorProps<any, false, any>
) => {
const { isFocused, selectProps } = props;
const color =
isFocused || selectProps.menuIsOpen
? "core-fleet-black"
: "ui-fleet-black-75";
return (
<components.DropdownIndicator {...props} className={baseClass}>
<Icon
name="chevron-down"
color={color}
className={`${baseClass}__icon`}
/>
</components.DropdownIndicator>
);
};
const [variableControlStyles, variableSingleValueStyles] = asFormField
? [
{
padding: ".5rem 1rem",
backgroundColor: COLORS["ui-light-grey"],
},
{},
]
: [
{
padding: "8px 0",
backgroundColor: "initial",
border: 0,
},
{
fontSize: "24px",
},
];
// see https://react-select.com/styles#the-styles-prop
const customStyles: StylesConfig<INumberDropdownOption, false> = {
control: (baseStyles, state) => ({
...baseStyles,
...variableControlStyles,
display: "flex",
flexDirection: "row",
borderRadius: "4px",
boxShadow: "none",
cursor: "pointer",
"&:hover": {
boxShadow: "none",
".team-dropdown__single-value": {
color: COLORS["core-fleet-black"],
},
".team-dropdown__indicator path": {
stroke: COLORS["ui-fleet-black-75-over"],
},
},
// When tabbing
// Relies on --is-focused for styling as &:focus-visible cannot be applied
"&.team-dropdown__control--is-focused": {
".team-dropdown__indicator path": {
stroke: COLORS["ui-fleet-black-75-over"],
},
},
...(state.isDisabled && {
".team-dropdown__single-value": {
color: COLORS["ui-fleet-black-50"],
},
".team-dropdown__indicator path": {
stroke: COLORS["ui-fleet-black-50"],
},
}),
// When clicking
"&:active": {
".team-dropdown__single-value": {
color: COLORS["ui-fleet-black-75-down"],
},
".team-dropdown__indicator path": {
stroke: COLORS["ui-fleet-black-75-down"],
},
},
...(state.menuIsOpen && {
".team-dropdown__indicator svg": {
transform: "rotate(180deg)",
transition: "transform 0.25s ease",
},
}),
}),
singleValue: (baseStyles) => ({
...baseStyles,
...variableSingleValueStyles,
lineHeight: "normal",
paddingLeft: 0,
paddingRight: "8px",
margin: 0,
// omit grid-column-end for automatic width
gridArea: "1/1/2",
}),
dropdownIndicator: (baseStyles) => ({
...baseStyles,
display: "flex",
padding: "2px",
margin: "0 5px",
svg: {
transition: "transform 0.25s ease",
},
}),
menu: (baseStyles) => ({
...baseStyles,
boxShadow: "0 2px 6px rgba(0, 0, 0, 0.1)",
borderRadius: "4px",
zIndex: 6,
overflow: "hidden",
border: 0,
marginTop: 0,
minWidth: "330px",
maxHeight: "none",
position: "absolute",
left: "0",
animation: "fade-in 150ms ease-out",
}),
// Placeholder is never shown on teams dropdown
menuList: (baseStyles) => ({
...baseStyles,
padding: PADDING["pad-small"],
".team-dropdown__menu-notice--no-options": {
textAlign: "left",
color: COLORS["ui-fleet-black-50"],
fontSize: FONT_SIZES["xx-small"],
fontWeight: FONT_WEIGHTS.regular,
},
}),
valueContainer: (baseStyles) => ({
...baseStyles,
padding: 0,
}),
input: (baseStyles) => ({
...baseStyles,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
margin: 0,
}),
option: (baseStyles, state) => ({
...baseStyles,
padding: "10px 8px",
fontSize: "13px",
borderRadius: "4px",
backgroundColor: getOptionBackgroundColor(state),
fontWeight: state.isSelected ? "600" : "normal",
color: COLORS["core-fleet-black"],
"&:hover": {
backgroundColor: state.isDisabled
? "transparent"
: COLORS["ui-fleet-black-5"],
},
"&:active": {
backgroundColor: state.isDisabled
? "transparent"
: COLORS["ui-fleet-black-5"],
},
...(state.isDisabled && {
color: COLORS["ui-fleet-black-50"],
fontStyle: "italic",
}),
}),
};
return (
<div className={dropdownWrapperClasses}>
<Select<INumberDropdownOption, false>
options={teamOptions}
placeholder="All teams"
onChange={(newValue) => {
if (newValue) {
onChange(newValue.value);
}
// If newValue is null or undefined, we don't call onChange
}}
isDisabled={isDisabled}
isSearchable
noOptionsMessage={() => "No matching teams"}
styles={customStyles}
components={{
DropdownIndicator: CustomDropdownIndicator,
IndicatorSeparator: () => null,
}}
value={teamOptions.find((option) => option.value === selectedValue)}
isOptionSelected={() => false} // Hides any styling on selected option
className={baseClass}
classNamePrefix={baseClass}
onMenuOpen={onOpen}
onMenuClose={onClose}
/>
</div>
);
};
export default TeamsDropdown;