Changed the group picker to reflect the updated design (#743)
This commit is contained in:
parent
6c0d52c761
commit
707d11fe93
5 changed files with 165 additions and 139 deletions
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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 ? (
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in a new issue