Changed the group picker to reflect the updated design (#743)

This commit is contained in:
Erik Jan de Wit 2021-07-06 11:31:14 +02:00 committed by GitHub
parent 6c0d52c761
commit 707d11fe93
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 165 additions and 139 deletions

View file

@ -20,11 +20,12 @@ import type GroupRepresentation from "keycloak-admin/lib/defs/groupRepresentatio
import { useAdminClient, useFetch } from "../../context/auth/AdminClient"; import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
import { ListEmptyState } from "../list-empty-state/ListEmptyState"; import { ListEmptyState } from "../list-empty-state/ListEmptyState";
import { PaginatingTableToolbar } from "../table-toolbar/PaginatingTableToolbar"; import { PaginatingTableToolbar } from "../table-toolbar/PaginatingTableToolbar";
import { GroupPath } from "./GroupPath";
export type GroupPickerDialogProps = { export type GroupPickerDialogProps = {
id?: string; id?: string;
type: "selectOne" | "selectMany"; type: "selectOne" | "selectMany";
filterGroups?: string[]; filterGroups?: GroupRepresentation[];
text: { title: string; ok: string }; text: { title: string; ok: string };
onConfirm: (groups: GroupRepresentation[]) => void; onConfirm: (groups: GroupRepresentation[]) => void;
onClose: () => void; onClose: () => void;
@ -48,7 +49,6 @@ export const GroupPickerDialog = ({
const [navigation, setNavigation] = useState<SelectableGroup[]>([]); const [navigation, setNavigation] = useState<SelectableGroup[]>([]);
const [groups, setGroups] = useState<SelectableGroup[]>([]); const [groups, setGroups] = useState<SelectableGroup[]>([]);
const [filtered, setFiltered] = useState<GroupRepresentation[]>();
const [filter, setFilter] = useState(""); const [filter, setFilter] = useState("");
const [joinedGroups, setJoinedGroups] = useState<GroupRepresentation[]>([]); const [joinedGroups, setJoinedGroups] = useState<GroupRepresentation[]>([]);
const [groupId, setGroupId] = useState<string>(); const [groupId, setGroupId] = useState<string>();
@ -60,23 +60,27 @@ export const GroupPickerDialog = ({
useFetch( useFetch(
async () => { async () => {
const allGroups = await adminClient.groups.find(); let group;
let groups;
let existingUserGroups;
if (!groupId) {
groups = await adminClient.groups.find({
first,
max: max + 1,
search: filter,
});
} else {
group = await adminClient.groups.findOne({ id: groupId });
groups = group.subGroups!;
}
if (groupId) { if (id) {
const group = await adminClient.groups.findOne({ id: groupId }); existingUserGroups = await adminClient.users.listGroups({
return { group, groups: group.subGroups! };
} else if (id) {
const existingUserGroups = await adminClient.users.listGroups({
id, id,
}); });
return { }
groups: allGroups,
existingUserGroups, return { group, groups, existingUserGroups };
};
} else
return {
groups: allGroups,
};
}, },
async ({ group: selectedGroup, groups, existingUserGroups }) => { async ({ group: selectedGroup, groups, existingUserGroups }) => {
setJoinedGroups(existingUserGroups || []); setJoinedGroups(existingUserGroups || []);
@ -87,36 +91,42 @@ export const GroupPickerDialog = ({
groups.forEach((group: SelectableGroup) => { groups.forEach((group: SelectableGroup) => {
group.checked = !!selectedRows.find((r) => r.id === group.id); group.checked = !!selectedRows.find((r) => r.id === group.id);
}); });
setFiltered(undefined); setGroups(groups);
setFilter("");
setFirst(0);
setMax(10);
setGroups(
filterGroups
? [
...groups.filter(
(row) => filterGroups && !filterGroups.includes(row.name!)
),
]
: groups
);
}, },
[groupId] [groupId, filter, first, max]
); );
const isRowDisabled = (row?: GroupRepresentation) => { const isRowDisabled = (row?: GroupRepresentation) => {
return !!joinedGroups.find((group) => group.id === row?.id); return !![...joinedGroups, ...(filterGroups || [])].find(
(group) => group.id === row?.id
);
}; };
const hasSubgroups = (group: GroupRepresentation) => { const hasSubgroups = (group: GroupRepresentation) => {
return group.subGroups!.length !== 0; return group.subGroups!.length !== 0;
}; };
const findSubGroup = (
group: GroupRepresentation,
name: string
): GroupRepresentation => {
if (group.name?.includes(name)) {
return group;
}
if (group.subGroups) {
for (const g of group.subGroups) {
const found = findSubGroup(g, name);
return found;
}
}
return group;
};
return ( return (
<Modal <Modal
variant={ModalVariant.small} variant={ModalVariant.small}
title={t(text.title, { title={t(text.title, {
group1: filterGroups && filterGroups[0], group1: filterGroups?.[0]?.name,
group2: currentGroup() ? currentGroup().name : t("root"), group2: currentGroup() ? currentGroup().name : t("root"),
})} })}
isOpen isOpen
@ -136,40 +146,8 @@ export const GroupPickerDialog = ({
</Button>, </Button>,
]} ]}
> >
<Breadcrumb>
{navigation.length > 0 && (
<BreadcrumbItem key="home">
<Button
variant="link"
onClick={() => {
setGroupId(undefined);
setNavigation([]);
}}
>
{t("groups")}
</Button>
</BreadcrumbItem>
)}
{navigation.map((group, i) => (
<BreadcrumbItem key={i}>
{navigation.length - 1 !== i && (
<Button
variant="link"
onClick={() => {
setGroupId(group.id);
setNavigation([...navigation].slice(0, i));
}}
>
{group.name}
</Button>
)}
{navigation.length - 1 === i && <>{group.name}</>}
</BreadcrumbItem>
))}
</Breadcrumb>
<PaginatingTableToolbar <PaginatingTableToolbar
count={(filtered || groups).slice(first, first + max).length} count={groups.length}
first={first} first={first}
max={max} max={max}
onNextClick={setFirst} onNextClick={setFirst}
@ -183,91 +161,136 @@ export const GroupPickerDialog = ({
setFilter(search); setFilter(search);
setFirst(0); setFirst(0);
setMax(10); setMax(10);
setFiltered( setNavigation([]);
groups.filter((group) =>
group.name?.toLowerCase().includes(search.toLowerCase())
)
);
}} }}
inputGroupPlaceholder={t("users:searchForGroups")} inputGroupPlaceholder={t("users:searchForGroups")}
> >
<DataList aria-label={t("groups")} isCompact> <Breadcrumb>
{(filtered || groups) {navigation.length > 0 && (
.slice(first, first + max) <BreadcrumbItem key="home">
.map((group: SelectableGroup) => ( <Button
<DataListItem variant="link"
aria-labelledby={group.name} onClick={() => {
key={group.id} setGroupId(undefined);
id={group.id} setNavigation([]);
onClick={(e) => {
if (type === "selectOne") {
setGroupId(group.id);
} else if (
hasSubgroups(group) &&
(e.target as HTMLInputElement).type !== "checkbox"
) {
setGroupId(group.id);
}
}} }}
> >
<DataListItemRow {t("groups")}
className={`join-group-dialog-row-${ </Button>
isRowDisabled(group) ? "m-disabled" : "" </BreadcrumbItem>
}`} )}
data-testid={group.name} {navigation.map((group, i) => (
<BreadcrumbItem key={i}>
{navigation.length - 1 !== i && (
<Button
variant="link"
onClick={() => {
setGroupId(group.id);
setNavigation([...navigation].slice(0, i));
}}
> >
{type === "selectMany" && ( {group.name}
<DataListCheck </Button>
className="join-group-modal-check" )}
data-testid={`${group.name}-check`} {navigation.length - 1 === i && <>{group.name}</>}
checked={group.checked} </BreadcrumbItem>
isDisabled={isRowDisabled(group)} ))}
onChange={(checked) => { </Breadcrumb>
group.checked = checked; <DataList aria-label={t("groups")} isCompact>
let newSelectedRows: SelectableGroup[] = []; {groups.slice(0, max).map((group: SelectableGroup) => (
if (!group.checked) { <DataListItem
newSelectedRows = selectedRows.filter( className={`join-group-dialog-row-${
(r) => r.id !== group.id isRowDisabled(group) ? "disabled" : ""
); }`}
} else if (group.checked) { aria-labelledby={group.name}
newSelectedRows = [...selectedRows, group]; key={group.id}
} id={group.id}
onClick={(e) => {
const g = filter !== "" ? findSubGroup(group, filter) : group;
if (isRowDisabled(g)) return;
if (type === "selectOne") {
setGroupId(g.id);
} else if (
hasSubgroups(group) &&
filter === "" &&
(e.target as HTMLInputElement).type !== "checkbox"
) {
setGroupId(group.id);
}
}}
>
<DataListItemRow
className={`join-group-dialog-row-${
isRowDisabled(group) ? "m-disabled" : ""
}`}
data-testid={group.name}
>
{type === "selectMany" && (
<DataListCheck
className="join-group-modal-check"
data-testid={`${group.name}-check`}
checked={group.checked}
isDisabled={isRowDisabled(group)}
onChange={(checked) => {
group.checked = checked;
let newSelectedRows: SelectableGroup[] = [];
if (!group.checked) {
newSelectedRows = selectedRows.filter(
(r) => r.id !== group.id
);
} else if (group.checked) {
newSelectedRows = [
...selectedRows,
filter === "" ? group : findSubGroup(group, filter),
];
}
setSelectedRows(newSelectedRows); setSelectedRows(newSelectedRows);
}} }}
aria-labelledby="data-list-check" aria-labelledby="data-list-check"
/>
)}
<DataListItemCells
dataListCells={[
<DataListCell key={`name-${group.id}`}>
<>{group.name}</>
</DataListCell>,
]}
/> />
<DataListAction )}
aria-labelledby={`select-${group.name}`}
id={`select-${group.name}`} <DataListItemCells
aria-label={t("groupName")} dataListCells={[
isPlainButtonAction <DataListCell key={`name-${group.id}`}>
> {filter === "" ? (
{(hasSubgroups(group) || type === "selectOne") && ( <>{group.name}</>
<Button isDisabled variant="link"> ) : (
<AngleRightIcon /> <GroupPath group={findSubGroup(group, filter)} />
</Button> )}
)} </DataListCell>,
</DataListAction> ]}
</DataListItemRow> />
</DataListItem> <DataListAction
))} aria-labelledby={`select-${group.name}`}
{(filtered || groups).length === 0 && filter === "" && ( id={`select-${group.name}`}
aria-label={t("groupName")}
isPlainButtonAction
>
{((hasSubgroups(group) && filter === "") ||
type === "selectOne") && (
<Button isDisabled variant="link">
<AngleRightIcon />
</Button>
)}
</DataListAction>
</DataListItemRow>
</DataListItem>
))}
{groups.length === 0 && filter === "" && (
<ListEmptyState <ListEmptyState
hasIcon={false} hasIcon={false}
message={t("groups:moveGroupEmpty")} message={t("groups:moveGroupEmpty")}
instructions={t("groups:moveGroupEmptyInstructions")} instructions={t("groups:moveGroupEmptyInstructions")}
/> />
)} )}
{groups.length === 0 && filter !== "" && (
<ListEmptyState
message={t("common:noSearchResults")}
instructions={t("common:noSearchResultsInstructions")}
/>
)}
</DataList> </DataList>
</PaginatingTableToolbar> </PaginatingTableToolbar>
</Modal> </Modal>

View file

@ -408,7 +408,7 @@ export function KeycloakDataTable<T>({
onCollapse={detailColumns ? onCollapse : undefined} onCollapse={detailColumns ? onCollapse : undefined}
actions={convertAction()} actions={convertAction()}
actionResolver={actionResolver} actionResolver={actionResolver}
rows={data} rows={data.slice(0, max)}
columns={columns} columns={columns}
isNotCompact={isNotCompact} isNotCompact={isNotCompact}
isRadio={isRadio} isRadio={isRadio}

View file

@ -194,7 +194,7 @@ export const GroupTable = () => {
{move && ( {move && (
<GroupPickerDialog <GroupPickerDialog
type="selectOne" type="selectOne"
filterGroups={[move.name!]} filterGroups={[move]}
text={{ text={{
title: "groups:moveToGroup", title: "groups:moveToGroup",
ok: "groups:moveHere", ok: "groups:moveHere",

View file

@ -152,7 +152,7 @@ export const UserForm = ({
setOpen(false); setOpen(false);
}} }}
onClose={() => setOpen(false)} onClose={() => setOpen(false)}
filterGroups={selectedGroups.map((g) => g.name!)} filterGroups={selectedGroups}
/> />
)} )}
{editMode && user ? ( {editMode && user ? (

View file

@ -15,6 +15,9 @@ kc-consents-chip-group .pf-c-chip-group__list {
button#kc-join-groups-button { button#kc-join-groups-button {
height: min-content; height: min-content;
} }
.join-group-dialog-row-disabled {
cursor: not-allowed;
}
.join-group-dialog-row-m-disabled { .join-group-dialog-row-m-disabled {
pointer-events: none; pointer-events: none;