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
@ -135,6 +145,25 @@ export const GroupPickerDialog = ({
{t(text.ok)} {t(text.ok)}
</Button>, </Button>,
]} ]}
>
<PaginatingTableToolbar
count={groups.length}
first={first}
max={max}
onNextClick={setFirst}
onPreviousClick={setFirst}
onPerPageSelect={(first, max) => {
setFirst(first);
setMax(max);
}}
inputGroupName={"common:search"}
inputGroupOnEnter={(search) => {
setFilter(search);
setFirst(0);
setMax(10);
setNavigation([]);
}}
inputGroupPlaceholder={t("users:searchForGroups")}
> >
<Breadcrumb> <Breadcrumb>
{navigation.length > 0 && ( {navigation.length > 0 && (
@ -167,43 +196,23 @@ export const GroupPickerDialog = ({
</BreadcrumbItem> </BreadcrumbItem>
))} ))}
</Breadcrumb> </Breadcrumb>
<PaginatingTableToolbar
count={(filtered || groups).slice(first, first + max).length}
first={first}
max={max}
onNextClick={setFirst}
onPreviousClick={setFirst}
onPerPageSelect={(first, max) => {
setFirst(first);
setMax(max);
}}
inputGroupName={"common:search"}
inputGroupOnEnter={(search) => {
setFilter(search);
setFirst(0);
setMax(10);
setFiltered(
groups.filter((group) =>
group.name?.toLowerCase().includes(search.toLowerCase())
)
);
}}
inputGroupPlaceholder={t("users:searchForGroups")}
>
<DataList aria-label={t("groups")} isCompact> <DataList aria-label={t("groups")} isCompact>
{(filtered || groups) {groups.slice(0, max).map((group: SelectableGroup) => (
.slice(first, first + max)
.map((group: SelectableGroup) => (
<DataListItem <DataListItem
className={`join-group-dialog-row-${
isRowDisabled(group) ? "disabled" : ""
}`}
aria-labelledby={group.name} aria-labelledby={group.name}
key={group.id} key={group.id}
id={group.id} id={group.id}
onClick={(e) => { onClick={(e) => {
const g = filter !== "" ? findSubGroup(group, filter) : group;
if (isRowDisabled(g)) return;
if (type === "selectOne") { if (type === "selectOne") {
setGroupId(group.id); setGroupId(g.id);
} else if ( } else if (
hasSubgroups(group) && hasSubgroups(group) &&
filter === "" &&
(e.target as HTMLInputElement).type !== "checkbox" (e.target as HTMLInputElement).type !== "checkbox"
) { ) {
setGroupId(group.id); setGroupId(group.id);
@ -230,7 +239,10 @@ export const GroupPickerDialog = ({
(r) => r.id !== group.id (r) => r.id !== group.id
); );
} else if (group.checked) { } else if (group.checked) {
newSelectedRows = [...selectedRows, group]; newSelectedRows = [
...selectedRows,
filter === "" ? group : findSubGroup(group, filter),
];
} }
setSelectedRows(newSelectedRows); setSelectedRows(newSelectedRows);
@ -242,7 +254,11 @@ export const GroupPickerDialog = ({
<DataListItemCells <DataListItemCells
dataListCells={[ dataListCells={[
<DataListCell key={`name-${group.id}`}> <DataListCell key={`name-${group.id}`}>
{filter === "" ? (
<>{group.name}</> <>{group.name}</>
) : (
<GroupPath group={findSubGroup(group, filter)} />
)}
</DataListCell>, </DataListCell>,
]} ]}
/> />
@ -252,7 +268,8 @@ export const GroupPickerDialog = ({
aria-label={t("groupName")} aria-label={t("groupName")}
isPlainButtonAction isPlainButtonAction
> >
{(hasSubgroups(group) || type === "selectOne") && ( {((hasSubgroups(group) && filter === "") ||
type === "selectOne") && (
<Button isDisabled variant="link"> <Button isDisabled variant="link">
<AngleRightIcon /> <AngleRightIcon />
</Button> </Button>
@ -261,13 +278,19 @@ export const GroupPickerDialog = ({
</DataListItemRow> </DataListItemRow>
</DataListItem> </DataListItem>
))} ))}
{(filtered || groups).length === 0 && filter === "" && ( {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;