Skip to content
Snippets Groups Projects
Unverified Commit 259560ca authored by Gustavo Saiani's avatar Gustavo Saiani Committed by GitHub
Browse files

Fix GroupSelect showing groups section title even if section is empty (#28348)

parent b208a205
No related branches found
No related tags found
No related merge requests found
Showing
with 199 additions and 44 deletions
/* eslint-disable react/prop-types */
import React from "react"; import React from "react";
import { t } from "ttag"; import { t } from "ttag";
import { isNotNull } from "metabase/core/utils/types";
import Icon from "metabase/components/Icon"; import Icon from "metabase/components/Icon";
import Select from "metabase/core/components/Select"; import Select from "metabase/core/components/Select";
import PopoverWithTrigger from "metabase/components/PopoverWithTrigger"; import PopoverWithTrigger from "metabase/components/PopoverWithTrigger";
...@@ -13,7 +13,39 @@ import { ...@@ -13,7 +13,39 @@ import {
getGroupColor, getGroupColor,
getGroupNameLocalized, getGroupNameLocalized,
} from "metabase/lib/groups"; } from "metabase/lib/groups";
import GroupSummary from "./GroupSummary"; import { GroupIds, UserGroupType, UserGroupsType } from "metabase/admin/types";
import GroupSummary from "../GroupSummary";
type GroupSelectProps = {
groups: UserGroupsType;
selectedGroupIds: GroupIds;
onGroupChange: (group: UserGroupType, selected: boolean) => void;
isCurrentUser?: boolean;
emptyListMessage?: string;
};
function getSections(groups: UserGroupsType) {
const adminGroup = groups.find(isAdminGroup);
const defaultGroup = groups.find(isDefaultGroup);
const topGroups = [defaultGroup, adminGroup].filter(g => g != null);
const groupsExceptDefaultAndAdmin = groups.filter(
g => !isAdminGroup(g) && !isDefaultGroup(g),
);
if (topGroups.length === 0) {
return [{ items: groupsExceptDefaultAndAdmin }];
}
return [
{ items: topGroups },
groupsExceptDefaultAndAdmin.length > 0
? {
items: groupsExceptDefaultAndAdmin as any,
name: t`Groups`,
}
: null,
].filter(isNotNull);
}
export const GroupSelect = ({ export const GroupSelect = ({
groups, groups,
...@@ -21,7 +53,7 @@ export const GroupSelect = ({ ...@@ -21,7 +53,7 @@ export const GroupSelect = ({
onGroupChange, onGroupChange,
isCurrentUser = false, isCurrentUser = false,
emptyListMessage = t`No groups`, emptyListMessage = t`No groups`,
}) => { }: GroupSelectProps) => {
const triggerElement = ( const triggerElement = (
<div className="flex align-center"> <div className="flex align-center">
<span className="mr1 text-medium"> <span className="mr1 text-medium">
...@@ -38,35 +70,32 @@ export const GroupSelect = ({ ...@@ -38,35 +70,32 @@ export const GroupSelect = ({
</PopoverWithTrigger> </PopoverWithTrigger>
); );
} }
const other = groups.filter(g => !isAdminGroup(g) && !isDefaultGroup(g));
const adminGroup = groups.find(isAdminGroup); const sections = getSections(groups);
const defaultGroup = groups.find(isDefaultGroup);
const topGroups = [defaultGroup, adminGroup].filter(g => g != null);
return ( return (
<Select <Select
triggerElement={triggerElement} triggerElement={triggerElement}
onChange={({ target: { value } }) => { onChange={({ target: { value } }: { target: { value: any } }) => {
groups groups
.filter( .filter(
// find the differing groups between the new `value` on previous `selectedGroupIds` // find the differing groups between the new `value` on previous `selectedGroupIds`
group => group =>
selectedGroupIds.includes(group.id) ^ value.includes(group.id), (selectedGroupIds.includes(group.id) as any) ^
value.includes(group.id),
) )
.forEach(group => onGroupChange(group, value.includes(group.id))); .forEach(group => onGroupChange(group, value.includes(group.id)));
}} }}
optionDisabledFn={group => optionDisabledFn={(group: UserGroupType) =>
(isAdminGroup(group) && isCurrentUser) || !canEditMembership(group) (isAdminGroup(group) && isCurrentUser) || !canEditMembership(group)
} }
optionValueFn={group => group.id} optionValueFn={(group: UserGroupType) => group.id}
optionNameFn={getGroupNameLocalized} optionNameFn={getGroupNameLocalized}
optionStylesFn={group => ({ color: getGroupColor(group) })} optionStylesFn={(group: UserGroupType) => ({
color: getGroupColor(group),
})}
value={selectedGroupIds} value={selectedGroupIds}
sections={ sections={sections}
topGroups.length > 0
? [{ items: topGroups }, { items: other, name: t`Groups` }]
: [{ items: other }]
}
multiple multiple
/> />
); );
......
import React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import GroupSelect from "./GroupSelect";
const adminGroup = { id: 2, name: "Administrators", member_count: 1 };
const setup = ({ groups = [adminGroup], selectedGroupIds = [] } = {}) => {
const onGroupChangeSpy = jest.fn();
render(
<GroupSelect
groups={groups}
selectedGroupIds={selectedGroupIds}
onGroupChange={onGroupChangeSpy}
/>,
);
return { onGroupChangeSpy };
};
describe("GroupSelect", () => {
afterEach(() => {
jest.clearAllMocks();
});
describe("when only the Administrators group is passed", () => {
it("should not show a Groups section (metabase#27728)", () => {
setup();
userEvent.click(screen.getByText("Default"));
expect(screen.queryByText("Groups")).not.toBeInTheDocument();
});
it("should allow you to select the Administrators group", () => {
const { onGroupChangeSpy } = setup();
userEvent.click(screen.getByText("Default"));
userEvent.click(screen.getByText("Administrators"));
expect(onGroupChangeSpy).toHaveBeenCalledTimes(1);
expect(onGroupChangeSpy).toHaveBeenCalledWith(adminGroup, true);
});
});
});
export { default } from "./GroupSelect";
...@@ -4,7 +4,7 @@ import { t } from "ttag"; ...@@ -4,7 +4,7 @@ import { t } from "ttag";
import Button from "metabase/core/components/Button"; import Button from "metabase/core/components/Button";
import type { MappingsType } from "../types"; import type { MappingsType } from "metabase/admin/types";
type AddMappingRowProps = { type AddMappingRowProps = {
mappings: MappingsType; mappings: MappingsType;
......
...@@ -6,7 +6,10 @@ import { ModalFooter } from "metabase/components/ModalContent"; ...@@ -6,7 +6,10 @@ import { ModalFooter } from "metabase/components/ModalContent";
import Radio from "metabase/core/components/Radio"; import Radio from "metabase/core/components/Radio";
import Button from "metabase/core/components/Button"; import Button from "metabase/core/components/Button";
import type { DeleteMappingModalValueType, GroupIds } from "../types"; import type {
DeleteMappingModalValueType,
GroupIds,
} from "metabase/admin/types";
import { import {
ModalHeader, ModalHeader,
ModalSubtitle, ModalSubtitle,
......
...@@ -63,9 +63,12 @@ describe("GroupMappingsWidget", () => { ...@@ -63,9 +63,12 @@ describe("GroupMappingsWidget", () => {
// Confirm remove // Confirm remove
userEvent.click(screen.getByText("Yes")); userEvent.click(screen.getByText("Yes"));
await waitFor(() => { await waitFor(
expect(onChangeSettingSpy).toHaveBeenCalledTimes(1); () => {
}); expect(onChangeSettingSpy).toHaveBeenCalledTimes(1);
},
{ timeout: 10000 },
);
}); });
}); });
......
import React from "react"; import React from "react";
import { t } from "ttag"; import { t } from "ttag";
import LoadingSpinner from "metabase/components/LoadingSpinner"; import { isNotNull } from "metabase/core/utils/types";
import GroupSelect from "metabase/admin/people/components/GroupSelect"; import Icon from "metabase/components/Icon";
import Select from "metabase/core/components/Select";
import PopoverWithTrigger from "metabase/components/PopoverWithTrigger";
import type { GroupIds, UserGroupsType } from "../types"; import {
isDefaultGroup,
isAdminGroup,
canEditMembership,
getGroupColor,
getGroupNameLocalized,
} from "metabase/lib/groups";
import { GroupIds, UserGroupType, UserGroupsType } from "metabase/admin/types";
import GroupSummary from "metabase/admin/people/components/GroupSummary";
type Props = { type GroupSelectProps = {
groups: UserGroupsType; groups: UserGroupsType;
selectedGroupIds: GroupIds; selectedGroupIds: GroupIds;
onGroupChange: () => void; onGroupChange: (group: UserGroupType, selected: boolean) => void;
isCurrentUser?: boolean;
emptyListMessage?: string;
}; };
const GroupMappingsWidgetGroupSelect = ({ function getSections(groups: UserGroupsType) {
const adminGroup = groups.find(isAdminGroup);
const defaultGroup = groups.find(isDefaultGroup);
const topGroups = [defaultGroup, adminGroup].filter(g => g != null);
const groupsExceptDefaultAndAdmin = groups.filter(
g => !isAdminGroup(g) && !isDefaultGroup(g),
);
if (topGroups.length === 0) {
return [{ items: groupsExceptDefaultAndAdmin }];
}
return [
{ items: topGroups },
groupsExceptDefaultAndAdmin.length > 0
? {
items: groupsExceptDefaultAndAdmin as any,
name: t`Groups`,
}
: null,
].filter(isNotNull);
}
export const GroupSelect = ({
groups, groups,
selectedGroupIds, selectedGroupIds = [],
onGroupChange, onGroupChange,
}: Props) => isCurrentUser = false,
groups ? ( emptyListMessage = t`No groups`,
<GroupSelect }: GroupSelectProps) => {
groups={groups} const triggerElement = (
selectedGroupIds={selectedGroupIds} <div className="flex align-center">
onGroupChange={onGroupChange} <span className="mr1 text-medium">
emptyListMessage={t`No mappable groups`} <GroupSummary groups={groups} selectedGroupIds={selectedGroupIds} />
</span>
<Icon className="text-light" name="chevrondown" size={10} />
</div>
);
if (groups.length === 0) {
return (
<PopoverWithTrigger triggerElement={triggerElement}>
<span className="p1">{emptyListMessage}</span>
</PopoverWithTrigger>
);
}
const sections = getSections(groups);
return (
<Select
triggerElement={triggerElement}
onChange={({ target: { value } }: { target: { value: any } }) => {
groups
.filter(
// find the differing groups between the new `value` on previous `selectedGroupIds`
group =>
(selectedGroupIds.includes(group.id) as any) ^
value.includes(group.id),
)
.forEach(group => onGroupChange(group, value.includes(group.id)));
}}
optionDisabledFn={(group: UserGroupType) =>
(isAdminGroup(group) && isCurrentUser) || !canEditMembership(group)
}
optionValueFn={(group: UserGroupType) => group.id}
optionNameFn={getGroupNameLocalized}
optionStylesFn={(group: UserGroupType) => ({
color: getGroupColor(group),
})}
value={selectedGroupIds}
sections={sections}
multiple
/> />
) : (
<LoadingSpinner />
); );
};
export default GroupMappingsWidgetGroupSelect; export default GroupSelect;
...@@ -8,14 +8,13 @@ import Tooltip from "metabase/core/components/Tooltip"; ...@@ -8,14 +8,13 @@ import Tooltip from "metabase/core/components/Tooltip";
import Icon from "metabase/components/Icon"; import Icon from "metabase/components/Icon";
import Confirm from "metabase/components/Confirm"; import Confirm from "metabase/components/Confirm";
import Selectbox from "../GroupSelect";
import DeleteGroupMappingModal from "../DeleteGroupMappingModal";
import type { import type {
DeleteMappingModalValueType, DeleteMappingModalValueType,
GroupIds, GroupIds,
UserGroupsType, UserGroupsType,
} from "../types"; } from "metabase/admin/types";
import Selectbox from "../GroupSelect";
import DeleteGroupMappingModal from "../DeleteGroupMappingModal";
import { DeleteMappingButton } from "./MappingRow.styled"; import { DeleteMappingButton } from "./MappingRow.styled";
......
...@@ -182,7 +182,7 @@ const enterLdapSettings = () => { ...@@ -182,7 +182,7 @@ const enterLdapSettings = () => {
const createMapping = name => { const createMapping = name => {
cy.button("New mapping").click(); cy.button("New mapping").click();
cy.focused().type(name); cy.findByPlaceholderText("cn=People,ou=Groups,dc=metabase,dc=com").type(name);
cy.button("Add").click(); cy.button("Add").click();
}; };
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment