Fleet UI: Update Dropdown to use react-select 5.4 and other cleanup (#24164)
This commit is contained in:
@@ -18,7 +18,7 @@ import {
|
||||
|
||||
import Icon from "components/Icon";
|
||||
|
||||
interface INumberDropdownOption extends Omit<IDropdownOption, "value"> {
|
||||
export interface INumberDropdownOption extends Omit<IDropdownOption, "value"> {
|
||||
value: number; // Redefine the value property to be just number
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// Used with old react-select dropdown and
|
||||
// New react-select-5 ActionsDropdown.tsx
|
||||
// New react-select-5: ActionsDropdown.tsx, DropdownWrapper.tsx
|
||||
.Select > .Select-menu-outer,
|
||||
.actions-dropdown {
|
||||
.actions-dropdown,
|
||||
.react-select__option {
|
||||
.is-disabled * {
|
||||
color: $ui-fleet-black-50;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
// stories/DropdownWrapper.stories.tsx
|
||||
|
||||
import React from "react";
|
||||
import { Meta, Story } from "@storybook/react";
|
||||
import DropdownWrapper, {
|
||||
IDropdownWrapper,
|
||||
CustomOptionType,
|
||||
} from "./DropdownWrapper";
|
||||
|
||||
// Define metadata for the story
|
||||
export default {
|
||||
title: "Components/DropdownWrapper",
|
||||
component: DropdownWrapper,
|
||||
argTypes: {
|
||||
onChange: { action: "changed" },
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
// Define a template for the stories
|
||||
const Template: Story<IDropdownWrapper> = (args) => (
|
||||
<DropdownWrapper {...args} />
|
||||
);
|
||||
|
||||
// Sample options to be used in the dropdown
|
||||
const sampleOptions: CustomOptionType[] = [
|
||||
{ label: "Option 1", value: "option1", helpText: "Help text for option 1" },
|
||||
{
|
||||
label: "Option 2",
|
||||
value: "option2",
|
||||
tooltipContent: "Tooltip for option 2",
|
||||
},
|
||||
{ label: "Option 3", value: "option3", isDisabled: true },
|
||||
];
|
||||
|
||||
// Default story
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
options: sampleOptions,
|
||||
name: "dropdown-example",
|
||||
label: "Select an option",
|
||||
};
|
||||
|
||||
// Disabled story
|
||||
export const Disabled = Template.bind({});
|
||||
Disabled.args = {
|
||||
...Default.args,
|
||||
isDisabled: true,
|
||||
};
|
||||
|
||||
// With Help Text story
|
||||
export const WithHelpText = Template.bind({});
|
||||
WithHelpText.args = {
|
||||
...Default.args,
|
||||
helpText: "This is some help text for the dropdown",
|
||||
};
|
||||
|
||||
// With Error story
|
||||
export const WithError = Template.bind({});
|
||||
WithError.args = {
|
||||
...Default.args,
|
||||
error: "This is an error message",
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
import React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import DropdownWrapper, { CustomOptionType } from "./DropdownWrapper";
|
||||
|
||||
const sampleOptions: CustomOptionType[] = [
|
||||
{
|
||||
label: "Option 1",
|
||||
value: "option1",
|
||||
tooltipContent: "Tooltip 1",
|
||||
helpText: "Help text 1",
|
||||
},
|
||||
{
|
||||
label: "Option 2",
|
||||
value: "option2",
|
||||
tooltipContent: "Tooltip 2",
|
||||
helpText: "Help text 2",
|
||||
},
|
||||
];
|
||||
|
||||
describe("DropdownWrapper Component", () => {
|
||||
const mockOnChange = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test("renders with help text", () => {
|
||||
render(
|
||||
<DropdownWrapper
|
||||
options={sampleOptions}
|
||||
value="option1"
|
||||
onChange={mockOnChange}
|
||||
name="test-dropdown"
|
||||
label="Test Dropdown"
|
||||
helpText="This is a help text."
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText(/test dropdown/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/this is a help text/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("calls onChange when an option is selected", async () => {
|
||||
render(
|
||||
<DropdownWrapper
|
||||
options={sampleOptions}
|
||||
value="option1"
|
||||
onChange={mockOnChange}
|
||||
name="test-dropdown"
|
||||
label="Test Dropdown"
|
||||
placeholder="Choose option"
|
||||
/>
|
||||
);
|
||||
|
||||
// Open the dropdown
|
||||
await userEvent.click(screen.getByText(/option 1/i));
|
||||
|
||||
// Select Option 2
|
||||
await userEvent.click(screen.getByText(/option 2/i));
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith({
|
||||
helpText: "Help text 2",
|
||||
label: "Option 2",
|
||||
tooltipContent: "Tooltip 2",
|
||||
value: "option2",
|
||||
});
|
||||
});
|
||||
|
||||
test("renders error message when provided", () => {
|
||||
render(
|
||||
<DropdownWrapper
|
||||
options={sampleOptions}
|
||||
value="option1"
|
||||
onChange={mockOnChange}
|
||||
name="test-dropdown"
|
||||
label="Test Dropdown"
|
||||
error="This is an error message."
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText(/this is an error message/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("displays no options message when no options are available", async () => {
|
||||
render(
|
||||
<DropdownWrapper
|
||||
options={[]}
|
||||
value=""
|
||||
onChange={mockOnChange}
|
||||
name="test-dropdown"
|
||||
label="Test Dropdown"
|
||||
placeholder="Choose option"
|
||||
/>
|
||||
);
|
||||
|
||||
// Open dropdown
|
||||
await userEvent.click(screen.getByText(/choose option/i));
|
||||
|
||||
expect(screen.getByText(/no results found/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,340 @@
|
||||
/**
|
||||
* This is a new component built off react-select 5.4
|
||||
* meant to replace Dropdown.jsx built off react-select 1.3
|
||||
*
|
||||
* See storybook component for current functionality
|
||||
*
|
||||
* Prototyped on UserForm.tsx but added and tested the following:
|
||||
* Options: text, disabled, option helptext, option tooltip
|
||||
* Other: label text, dropdown help text, dropdown error
|
||||
*/
|
||||
|
||||
import classnames from "classnames";
|
||||
import React from "react";
|
||||
import Select, {
|
||||
StylesConfig,
|
||||
DropdownIndicatorProps,
|
||||
OptionProps,
|
||||
components,
|
||||
PropsValue,
|
||||
SingleValue,
|
||||
} from "react-select-5";
|
||||
|
||||
import { COLORS } from "styles/var/colors";
|
||||
import { PADDING } from "styles/var/padding";
|
||||
|
||||
import FormField from "components/forms/FormField";
|
||||
import DropdownOptionTooltipWrapper from "components/forms/fields/Dropdown/DropdownOptionTooltipWrapper";
|
||||
import Icon from "components/Icon";
|
||||
|
||||
const getOptionBackgroundColor = (state: any) => {
|
||||
return state.isSelected || state.isFocused
|
||||
? COLORS["ui-vibrant-blue-10"]
|
||||
: "transparent";
|
||||
};
|
||||
|
||||
export interface CustomOptionType {
|
||||
label: string;
|
||||
value: string;
|
||||
tooltipContent?: string;
|
||||
helpText?: string;
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
||||
export interface IDropdownWrapper {
|
||||
options: CustomOptionType[];
|
||||
value?: PropsValue<CustomOptionType> | string;
|
||||
onChange: (newValue: SingleValue<CustomOptionType>) => void;
|
||||
name: string;
|
||||
className?: string;
|
||||
labelClassname?: string;
|
||||
error?: string;
|
||||
label?: JSX.Element | string;
|
||||
helpText?: JSX.Element | string;
|
||||
isSearchable?: boolean;
|
||||
isDisabled?: boolean;
|
||||
placeholder?: string;
|
||||
menuPortalTarget?: HTMLElement | null;
|
||||
}
|
||||
|
||||
const baseClass = "dropdown-wrapper";
|
||||
|
||||
const DropdownWrapper = ({
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
name,
|
||||
className,
|
||||
labelClassname,
|
||||
error,
|
||||
label,
|
||||
helpText,
|
||||
isSearchable,
|
||||
isDisabled = false,
|
||||
placeholder,
|
||||
menuPortalTarget,
|
||||
}: IDropdownWrapper) => {
|
||||
const wrapperClassNames = classnames(baseClass, className);
|
||||
|
||||
const handleChange = (newValue: SingleValue<CustomOptionType>) => {
|
||||
onChange(newValue);
|
||||
};
|
||||
|
||||
// Ability to handle value of type string or CustomOptionType
|
||||
const getCurrentValue = () => {
|
||||
if (typeof value === "string") {
|
||||
return options.find((option) => option.value === value) || null;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
interface CustomOptionProps
|
||||
extends Omit<OptionProps<CustomOptionType, false>, "data"> {
|
||||
data: CustomOptionType;
|
||||
}
|
||||
|
||||
const CustomOption = (props: CustomOptionProps) => {
|
||||
const { data, ...rest } = props;
|
||||
|
||||
const optionContent = (
|
||||
<div className={`${baseClass}__option`}>
|
||||
{data.label}
|
||||
{data.helpText && (
|
||||
<span className={`${baseClass}__help-text`}>{data.helpText}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<components.Option {...rest} data={data}>
|
||||
{data.tooltipContent ? (
|
||||
<DropdownOptionTooltipWrapper tipContent={data.tooltipContent}>
|
||||
{optionContent}
|
||||
</DropdownOptionTooltipWrapper>
|
||||
) : (
|
||||
optionContent
|
||||
)}
|
||||
</components.Option>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomDropdownIndicator = (
|
||||
props: DropdownIndicatorProps<any, false, any>
|
||||
) => {
|
||||
const { isFocused, selectProps } = props;
|
||||
const color =
|
||||
isFocused || selectProps.menuIsOpen
|
||||
? "core-fleet-blue"
|
||||
: "core-fleet-black";
|
||||
|
||||
return (
|
||||
<components.DropdownIndicator
|
||||
{...props}
|
||||
className={`${baseClass}__indicator`}
|
||||
>
|
||||
<Icon
|
||||
name="chevron-down"
|
||||
color={color}
|
||||
className={`${baseClass}__icon`}
|
||||
/>
|
||||
</components.DropdownIndicator>
|
||||
);
|
||||
};
|
||||
|
||||
const customStyles: StylesConfig<CustomOptionType, false> = {
|
||||
container: (provided) => ({
|
||||
...provided,
|
||||
width: "100%",
|
||||
height: "40px",
|
||||
}),
|
||||
control: (provided, state) => ({
|
||||
...provided,
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
width: "100%",
|
||||
backgroundColor: COLORS["ui-off-white"],
|
||||
paddingLeft: "8px", // TODO: Update to match styleguide of (16px) when updating rest of UI (8px)
|
||||
paddingRight: "8px",
|
||||
cursor: "pointer",
|
||||
boxShadow: "none",
|
||||
borderRadius: "4px",
|
||||
borderColor: state.isFocused
|
||||
? COLORS["core-fleet-blue"]
|
||||
: COLORS["ui-fleet-black-10"],
|
||||
"&:hover": {
|
||||
boxShadow: "none",
|
||||
borderColor: COLORS["core-fleet-blue"],
|
||||
".dropdown-wrapper__single-value": {
|
||||
color: COLORS["core-vibrant-blue-over"],
|
||||
},
|
||||
".dropdown-wrapper__indicator path": {
|
||||
stroke: COLORS["core-vibrant-blue-over"],
|
||||
},
|
||||
},
|
||||
// When tabbing
|
||||
// Relies on --is-focused for styling as &:focus-visible cannot be applied
|
||||
"&.dropdown-wrapper__control--is-focused": {
|
||||
".dropdown-wrapper__single-value": {
|
||||
color: COLORS["core-vibrant-blue-over"],
|
||||
},
|
||||
".dropdown-wrapper__indicator path": {
|
||||
stroke: COLORS["core-vibrant-blue-over"],
|
||||
},
|
||||
},
|
||||
...(state.isDisabled && {
|
||||
".dropdown-wrapper__single-value": {
|
||||
color: COLORS["ui-fleet-black-50"],
|
||||
},
|
||||
".dropdown-wrapper__indicator path": {
|
||||
stroke: COLORS["ui-fleet-black-50"],
|
||||
},
|
||||
}),
|
||||
"&:active": {
|
||||
".dropdown-wrapper__single-value": {
|
||||
color: COLORS["core-vibrant-blue-down"],
|
||||
},
|
||||
".dropdown-wrapper__indicator path": {
|
||||
stroke: COLORS["core-vibrant-blue-down"],
|
||||
},
|
||||
},
|
||||
...(state.menuIsOpen && {
|
||||
".dropdown-wrapper__indicator svg": {
|
||||
transform: "rotate(180deg)",
|
||||
transition: "transform 0.25s ease",
|
||||
},
|
||||
}),
|
||||
}),
|
||||
singleValue: (provided) => ({
|
||||
...provided,
|
||||
fontSize: "16px",
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
}),
|
||||
dropdownIndicator: (provided) => ({
|
||||
...provided,
|
||||
display: "flex",
|
||||
padding: "2px",
|
||||
svg: {
|
||||
transition: "transform 0.25s ease",
|
||||
},
|
||||
}),
|
||||
menu: (provided) => ({
|
||||
...provided,
|
||||
boxShadow: "0 2px 6px rgba(0, 0, 0, 0.1)",
|
||||
borderRadius: "4px",
|
||||
zIndex: 6,
|
||||
overflow: "hidden",
|
||||
border: 0,
|
||||
marginTop: 0,
|
||||
maxHeight: "none",
|
||||
position: "absolute",
|
||||
left: "0",
|
||||
animation: "fade-in 150ms ease-out",
|
||||
}),
|
||||
menuList: (provided) => ({
|
||||
...provided,
|
||||
padding: PADDING["pad-small"],
|
||||
}),
|
||||
valueContainer: (provided) => ({
|
||||
...provided,
|
||||
padding: 0,
|
||||
}),
|
||||
option: (provided, state) => ({
|
||||
...provided,
|
||||
padding: "10px 8px",
|
||||
fontSize: "14px",
|
||||
backgroundColor: getOptionBackgroundColor(state),
|
||||
color: COLORS["core-fleet-black"],
|
||||
"&:hover": {
|
||||
backgroundColor: state.isDisabled
|
||||
? "transparent"
|
||||
: COLORS["ui-vibrant-blue-10"],
|
||||
},
|
||||
"&:active": {
|
||||
backgroundColor: state.isDisabled
|
||||
? "transparent"
|
||||
: COLORS["ui-vibrant-blue-10"],
|
||||
},
|
||||
...(state.isDisabled && {
|
||||
color: COLORS["ui-fleet-black-50"],
|
||||
fontStyle: "italic",
|
||||
cursor: "not-allowed",
|
||||
pointerEvents: "none",
|
||||
}),
|
||||
// Styles for custom option
|
||||
".dropdown-wrapper__option": {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "8px",
|
||||
width: "100%",
|
||||
},
|
||||
".dropdown-wrapper__help-text": {
|
||||
fontSize: "12px",
|
||||
whiteSpace: "normal",
|
||||
color: COLORS["ui-fleet-black-50"],
|
||||
fontStyle: "italic",
|
||||
},
|
||||
}),
|
||||
menuPortal: (base) => ({ ...base, zIndex: 999 }), // Not hidden beneath scrollable sections
|
||||
noOptionsMessage: (provided) => ({
|
||||
...provided,
|
||||
textAlign: "left",
|
||||
fontSize: "14px",
|
||||
padding: "10px 8px",
|
||||
}),
|
||||
};
|
||||
|
||||
const renderLabel = () => {
|
||||
const labelWrapperClasses = classnames(
|
||||
`${baseClass}__label`,
|
||||
labelClassname,
|
||||
{
|
||||
[`${baseClass}__label--error`]: !!error,
|
||||
[`${baseClass}__label--disabled`]: isDisabled,
|
||||
}
|
||||
);
|
||||
|
||||
if (!label) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return (
|
||||
<label className={labelWrapperClasses} htmlFor={name}>
|
||||
{error || label}
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormField
|
||||
name={name}
|
||||
label={renderLabel()}
|
||||
helpText={helpText}
|
||||
type="dropdown"
|
||||
className={wrapperClassNames}
|
||||
>
|
||||
<Select<CustomOptionType, false>
|
||||
classNamePrefix="react-select"
|
||||
isSearchable={isSearchable}
|
||||
styles={customStyles}
|
||||
options={options}
|
||||
components={{
|
||||
Option: CustomOption,
|
||||
DropdownIndicator: CustomDropdownIndicator,
|
||||
IndicatorSeparator: () => null,
|
||||
}}
|
||||
value={getCurrentValue()}
|
||||
onChange={handleChange}
|
||||
isDisabled={isDisabled}
|
||||
menuPortalTarget={
|
||||
menuPortalTarget === undefined ? document.body : menuPortalTarget
|
||||
}
|
||||
noOptionsMessage={() => "No results found"}
|
||||
tabIndex={isDisabled ? -1 : 0} // Ensures disabled dropdown has no keyboard accessibility
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</FormField>
|
||||
);
|
||||
};
|
||||
|
||||
export default DropdownWrapper;
|
||||
@@ -0,0 +1,14 @@
|
||||
// react-select's <Select/> styles prop customizes the styling of
|
||||
// the internal components and not external elements like labels
|
||||
// See customStyles in DropdownWrappr.tsx
|
||||
// https://react-select.com/styles
|
||||
.dropdown-wrapper {
|
||||
&__label {
|
||||
&--error {
|
||||
color: $ui-error;
|
||||
}
|
||||
&--disabled {
|
||||
color: $ui-fleet-black-50;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./DropdownWrapper";
|
||||
@@ -1,16 +0,0 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { UserRole } from "./user";
|
||||
|
||||
export default PropTypes.shape({
|
||||
disabled: PropTypes.bool,
|
||||
label: PropTypes.string,
|
||||
value: PropTypes.any, // eslint-disable-line react/forbid-prop-types
|
||||
helpText: PropTypes.string,
|
||||
});
|
||||
|
||||
export interface IRole {
|
||||
disabled: boolean;
|
||||
label: string;
|
||||
value: UserRole;
|
||||
helpText?: string;
|
||||
}
|
||||
@@ -1,10 +1,4 @@
|
||||
import React, {
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useState,
|
||||
useEffect,
|
||||
} from "react";
|
||||
import React, { useCallback, useContext, useMemo, useState } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { Link } from "react-router";
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import React from "react";
|
||||
import { InjectedRouter } from "react-router";
|
||||
import SandboxGate from "components/Sandbox/SandboxGate";
|
||||
import SandboxMessage from "components/Sandbox/SandboxMessage";
|
||||
import UsersTable from "./components/UsersTable";
|
||||
|
||||
const baseClass = "user-management";
|
||||
@@ -17,17 +15,7 @@ const UserManagementPage = ({ router }: IUserManagementProps): JSX.Element => {
|
||||
Create new users, customize user permissions, and remove users from
|
||||
Fleet.
|
||||
</p>
|
||||
<SandboxGate
|
||||
fallbackComponent={() => (
|
||||
<SandboxMessage
|
||||
message="User management is only available in self-managed Fleet"
|
||||
utmSource="fleet-ui-users-page"
|
||||
className={`${baseClass}__sandbox-message`}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<UsersTable router={router} />
|
||||
</SandboxGate>
|
||||
<UsersTable router={router} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+27
-38
@@ -1,10 +1,9 @@
|
||||
import React, { useState, useContext } from "react";
|
||||
|
||||
import { ITeam } from "interfaces/team";
|
||||
import { UserRole } from "interfaces/user";
|
||||
// ignore TS error for now until these are rewritten in ts.
|
||||
// @ts-ignore
|
||||
import Dropdown from "components/forms/fields/Dropdown";
|
||||
import { SingleValue } from "react-select-5";
|
||||
import DropdownWrapper from "components/forms/fields/DropdownWrapper";
|
||||
import { CustomOptionType } from "components/forms/fields/DropdownWrapper/DropdownWrapper";
|
||||
import { AppContext } from "context/app";
|
||||
import { roleOptions } from "../../helpers/userManagementHelpers";
|
||||
|
||||
@@ -13,29 +12,19 @@ interface ISelectRoleFormProps {
|
||||
currentTeam?: ITeam;
|
||||
teams: ITeam[];
|
||||
onFormChange: (teams: ITeam[]) => void;
|
||||
label: string | string[];
|
||||
isApiOnly?: boolean;
|
||||
}
|
||||
|
||||
const generateSelectedTeamData = (
|
||||
allTeams: ITeam[],
|
||||
updatedTeam?: any
|
||||
updatedTeam?: Partial<ITeam>
|
||||
): ITeam[] => {
|
||||
const filtered = allTeams.map(
|
||||
(teamItem): ITeam => {
|
||||
const teamRole =
|
||||
teamItem.id === updatedTeam?.id ? updatedTeam.role : teamItem.role;
|
||||
return {
|
||||
description: teamItem.description,
|
||||
id: teamItem.id,
|
||||
host_count: teamItem.host_count,
|
||||
user_count: teamItem.user_count,
|
||||
name: teamItem.name,
|
||||
role: teamRole,
|
||||
};
|
||||
}
|
||||
return allTeams.map(
|
||||
(teamItem): ITeam => ({
|
||||
...teamItem,
|
||||
role: teamItem.id === updatedTeam?.id ? updatedTeam.role! : teamItem.role,
|
||||
})
|
||||
);
|
||||
return filtered;
|
||||
};
|
||||
|
||||
const SelectRoleForm = ({
|
||||
@@ -43,33 +32,33 @@ const SelectRoleForm = ({
|
||||
currentTeam,
|
||||
teams,
|
||||
onFormChange,
|
||||
label,
|
||||
isApiOnly,
|
||||
}: ISelectRoleFormProps): JSX.Element => {
|
||||
const { isPremiumTier } = useContext(AppContext);
|
||||
|
||||
const [selectedRole, setSelectedRole] = useState(
|
||||
defaultTeamRole.toLowerCase()
|
||||
);
|
||||
const [selectedRole, setSelectedRole] = useState<CustomOptionType>({
|
||||
value: defaultTeamRole.toLowerCase(),
|
||||
label: defaultTeamRole,
|
||||
});
|
||||
|
||||
const updateSelectedRole = (newRoleValue: UserRole) => {
|
||||
const updatedTeam = { ...currentTeam };
|
||||
|
||||
updatedTeam.role = newRoleValue;
|
||||
|
||||
onFormChange(generateSelectedTeamData(teams, updatedTeam));
|
||||
|
||||
setSelectedRole(newRoleValue);
|
||||
const updateSelectedRole = (newRoleValue: SingleValue<CustomOptionType>) => {
|
||||
if (newRoleValue) {
|
||||
const updatedTeam = {
|
||||
...currentTeam,
|
||||
role: newRoleValue.value as UserRole,
|
||||
};
|
||||
onFormChange(generateSelectedTeamData(teams, updatedTeam));
|
||||
setSelectedRole(newRoleValue);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
label={label}
|
||||
value={selectedRole}
|
||||
<DropdownWrapper
|
||||
name="Team role"
|
||||
options={roleOptions({ isPremiumTier, isApiOnly })}
|
||||
searchable={false}
|
||||
onChange={(newRoleValue: UserRole) => updateSelectedRole(newRoleValue)}
|
||||
testId={`${name}-checkbox`}
|
||||
value={selectedRole}
|
||||
onChange={updateSelectedRole}
|
||||
isSearchable={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+11
-10
@@ -3,8 +3,9 @@ import React, { useState } from "react";
|
||||
import { ITeam } from "interfaces/team";
|
||||
import { UserRole } from "interfaces/user";
|
||||
import Checkbox from "components/forms/fields/Checkbox";
|
||||
// @ts-ignore
|
||||
import Dropdown from "components/forms/fields/Dropdown";
|
||||
import { SingleValue } from "react-select-5";
|
||||
import DropdownWrapper from "components/forms/fields/DropdownWrapper";
|
||||
import { CustomOptionType } from "components/forms/fields/DropdownWrapper/DropdownWrapper";
|
||||
import { roleOptions } from "../../helpers/userManagementHelpers";
|
||||
|
||||
interface ITeamCheckboxListItem extends ITeam {
|
||||
@@ -62,7 +63,7 @@ const generateSelectedTeamData = (
|
||||
const updateFormState = (
|
||||
prevTeamItems: ITeamCheckboxListItem[],
|
||||
teamId: number,
|
||||
newValue: UserRole | boolean | undefined
|
||||
newValue: SingleValue<CustomOptionType> | boolean | undefined
|
||||
): ITeamCheckboxListItem[] => {
|
||||
const prevItemIndex = prevTeamItems.findIndex((item) => item.id === teamId);
|
||||
const prevItem = prevTeamItems[prevItemIndex];
|
||||
@@ -70,7 +71,7 @@ const updateFormState = (
|
||||
if (typeof newValue === "boolean") {
|
||||
prevItem.isChecked = newValue;
|
||||
} else {
|
||||
prevItem.role = newValue;
|
||||
prevItem.role = newValue?.value as UserRole;
|
||||
}
|
||||
|
||||
return [...prevTeamItems];
|
||||
@@ -87,7 +88,7 @@ const useSelectedTeamState = (
|
||||
|
||||
const updateSelectedTeams = (
|
||||
teamId: number,
|
||||
newValue: UserRole | boolean
|
||||
newValue: CustomOptionType | boolean
|
||||
) => {
|
||||
setTeamsFormList((prevState) => {
|
||||
const updatedTeamFormList = updateFormState(prevState, teamId, newValue);
|
||||
@@ -127,15 +128,15 @@ const SelectedTeamsForm = ({
|
||||
>
|
||||
{name}
|
||||
</Checkbox>
|
||||
<Dropdown
|
||||
<DropdownWrapper
|
||||
name={name}
|
||||
value={role}
|
||||
className={`${baseClass}__role-dropdown`}
|
||||
options={roleOptions({ isPremiumTier: true, isApiOnly })}
|
||||
searchable={false}
|
||||
onChange={(newValue: UserRole) =>
|
||||
updateSelectedTeams(teamItem.id, newValue)
|
||||
isSearchable={false}
|
||||
onChange={(newValue: SingleValue<CustomOptionType>) =>
|
||||
updateSelectedTeams(teamItem.id, newValue as CustomOptionType)
|
||||
}
|
||||
testId={`${name}-checkbox`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
justify-content: space-between;
|
||||
|
||||
.form-field--dropdown {
|
||||
width: auto;
|
||||
width: 154px; // Matches dropdown
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -370,7 +370,6 @@ const UserForm = ({
|
||||
</>
|
||||
) : (
|
||||
<SelectRoleForm
|
||||
label="Role"
|
||||
currentTeam={currentTeam || formData.teams[0]}
|
||||
teams={formData.teams}
|
||||
defaultTeamRole={defaultTeamRole || "observer"}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { isEqual } from "lodash";
|
||||
|
||||
import { CustomOptionType } from "components/forms/fields/DropdownWrapper/DropdownWrapper";
|
||||
import { IInvite } from "interfaces/invite";
|
||||
import { IUser, IUserUpdateBody, IUpdateUserFormData } from "interfaces/user";
|
||||
import { IRole } from "interfaces/role";
|
||||
import { IFormData } from "../components/UserForm/UserForm";
|
||||
|
||||
type ICurrentUserData = Pick<
|
||||
@@ -10,7 +10,7 @@ type ICurrentUserData = Pick<
|
||||
"global_role" | "teams" | "name" | "email" | "sso_enabled"
|
||||
>;
|
||||
|
||||
interface IRoleOptionsParams {
|
||||
export interface IRoleOptionsParams {
|
||||
isPremiumTier?: boolean;
|
||||
isApiOnly?: boolean;
|
||||
}
|
||||
@@ -58,20 +58,17 @@ const generateUpdateData = (
|
||||
export const roleOptions = ({
|
||||
isPremiumTier,
|
||||
isApiOnly,
|
||||
}: IRoleOptionsParams): IRole[] => {
|
||||
const roles: IRole[] = [
|
||||
}: IRoleOptionsParams): CustomOptionType[] => {
|
||||
const roles: CustomOptionType[] = [
|
||||
{
|
||||
disabled: false,
|
||||
label: "Observer",
|
||||
value: "observer",
|
||||
},
|
||||
{
|
||||
disabled: false,
|
||||
label: "Maintainer",
|
||||
value: "maintainer",
|
||||
},
|
||||
{
|
||||
disabled: false,
|
||||
label: "Admin",
|
||||
value: "admin",
|
||||
},
|
||||
@@ -79,14 +76,12 @@ export const roleOptions = ({
|
||||
|
||||
if (isPremiumTier) {
|
||||
roles.splice(1, 0, {
|
||||
disabled: false,
|
||||
label: "Observer+",
|
||||
value: "observer_plus",
|
||||
});
|
||||
|
||||
if (isApiOnly) {
|
||||
roles.splice(3, 0, {
|
||||
disabled: false,
|
||||
label: "GitOps",
|
||||
value: "gitops",
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user