added invite user dialog

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
This commit is contained in:
Erik Jan de Wit 2024-08-16 14:09:57 +02:00 committed by Pedro Igor
parent e6dd8ac1c0
commit 2b0392a3e8
5 changed files with 87 additions and 25 deletions

View file

@ -3222,7 +3222,8 @@ emptyUserOrganizationsInstructions=There is no organization yet. Please join an
joinOrganization=Join organization
sendInvitation=Send invitation
removeConfirmOrganizationTitle=Remove organization?
organizationRemoveConfirm=Are you sure you want to remove user from the {{count}} selected organizations?
organizationRemoveConfirm_one=Are you sure you want to remove user from the selected organization?
organizationRemoveConfirm_other=Are you sure you want to remove user from the {{count}} selected organizations?
organizationRemovedSuccess=User removed from organizations
organizationRemoveError=Could not remove user from organizations\: {{error}}
organizationName=Organization name
@ -3231,3 +3232,4 @@ join=Join
userAddedOrganization_one=Organization added to the user
userAddedOrganizationError=Could not add organizations to the user\: {{error}}
userAddedOrganization_other={{count}} organizations added to the user
sentInvitation=Sent invitation

View file

@ -1,17 +1,23 @@
import OrganizationRepresentation from "@keycloak/keycloak-admin-client/lib/defs/organizationRepresentation";
import UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation";
import { Button, Modal, ModalVariant } from "@patternfly/react-core";
import { differenceBy } from "lodash-es";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useAdminClient } from "../admin-client";
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
import { TableText } from "@patternfly/react-table";
type OrganizationModalProps = {
isJoin?: boolean;
existingOrgs: OrganizationRepresentation[];
onAdd: (orgs: OrganizationRepresentation[]) => Promise<void>;
onClose: () => void;
};
export const OrganizationModal = ({
isJoin = true,
existingOrgs,
onAdd,
onClose,
}: OrganizationModalProps) => {
@ -21,13 +27,20 @@ export const OrganizationModal = ({
const [selectedRows, setSelectedRows] = useState<UserRepresentation[]>([]);
const loader = async (first?: number, max?: number, search?: string) => {
return await adminClient.organizations.find({ first, max, search });
const params = {
first,
search,
max: max! + existingOrgs.length,
};
const orgs = await adminClient.organizations.find(params);
return differenceBy(orgs, existingOrgs, "id");
};
return (
<Modal
variant={ModalVariant.small}
title={t("joinOrganization")}
title={isJoin ? t("joinOrganization") : t("sendInvitation")}
isOpen
onClose={onClose}
actions={[
@ -40,7 +53,7 @@ export const OrganizationModal = ({
onClose();
}}
>
{t("join")}
{isJoin ? t("join") : t("send")}
</Button>,
<Button
data-testid="cancel"
@ -66,6 +79,9 @@ export const OrganizationModal = ({
},
{
name: "description",
cellRenderer: (row) => (
<TableText wrapModifier="truncate">{row.description}</TableText>
),
},
]}
/>

View file

@ -57,7 +57,9 @@ const Domains = (org: OrganizationRepresentation) => {
};
type OrganizationTableProps = PropsWithChildren & {
loader: LoaderFunction<OrganizationRepresentation>;
loader:
| LoaderFunction<OrganizationRepresentation>
| OrganizationRepresentation[];
toolbarItem?: ReactNode;
isPaginated?: boolean;
onSelect?: (orgs: OrganizationRepresentation[]) => void;

View file

@ -17,6 +17,7 @@ import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
import { OrganizationModal } from "../organizations/OrganizationModal";
import { OrganizationTable } from "../organizations/OrganizationTable";
import { useFetch } from "../utils/useFetch";
import useToggle from "../utils/useToggle";
import { UserParams } from "./routes/User";
@ -30,11 +31,19 @@ export const Organizations = () => {
const refresh = () => setKey(key + 1);
const [joinToggle, toggle, setJoinToggle] = useToggle();
const [joinOrganization, setJoinOrganization] = useState(false);
const [shouldJoin, setShouldJoin] = useState(true);
const [openOrganizationPicker, setOpenOrganizationPicker] = useState(false);
const [userOrgs, setUserOrgs] = useState<OrganizationRepresentation[]>([]);
const [selectedOrgs, setSelectedOrgs] = useState<
OrganizationRepresentation[]
>([]);
useFetch(
() => adminClient.organizations.memberOrganizations({ userId: id! }),
setUserOrgs,
[key],
);
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
titleKey: "removeConfirmOrganizationTitle",
messageKey: t("organizationRemoveConfirm", { count: selectedOrgs.length }),
@ -51,6 +60,7 @@ export const Organizations = () => {
),
);
addAlert(t("organizationRemovedSuccess"));
setSelectedOrgs([]);
refresh();
} catch (error) {
addError("organizationRemoveError", error);
@ -60,18 +70,29 @@ export const Organizations = () => {
return (
<>
{joinOrganization && (
{openOrganizationPicker && (
<OrganizationModal
onClose={() => setJoinOrganization(false)}
isJoin={shouldJoin}
existingOrgs={userOrgs}
onClose={() => setOpenOrganizationPicker(false)}
onAdd={async (orgs) => {
try {
await Promise.all(
orgs.map((org) =>
adminClient.organizations.addMember({
orgId: org.id!,
userId: id!,
}),
),
orgs.map((org) => {
const form = new FormData();
form.append("id", id!);
return shouldJoin
? adminClient.organizations.addMember({
orgId: org.id!,
userId: id!,
})
: adminClient.organizations.inviteExistingUser(
{
orgId: org.id!,
},
form,
);
}),
);
addAlert(t("userAddedOrganization", { count: orgs.length }));
refresh();
@ -83,14 +104,13 @@ export const Organizations = () => {
)}
<DeleteConfirm />
<OrganizationTable
key={key}
loader={() =>
adminClient.organizations.memberOrganizations({
userId: id!,
})
}
loader={userOrgs}
onSelect={(orgs) => setSelectedOrgs(orgs)}
deleteLabel="remove"
onDelete={(org) => {
setSelectedOrgs([org]);
toggleDeleteDialog();
}}
toolbarItem={
<>
<ToolbarItem>
@ -112,13 +132,20 @@ export const Organizations = () => {
<DropdownItem
key="join"
onClick={() => {
setJoinOrganization(true);
setShouldJoin(true);
setOpenOrganizationPicker(true);
}}
>
{t("joinOrganization")}
</DropdownItem>
<DropdownItem key="invite" component="button">
{t("invite")}
<DropdownItem
key="invite"
onClick={() => {
setShouldJoin(false);
setOpenOrganizationPicker(true);
}}
>
{t("sentInvite")}
</DropdownItem>
</DropdownList>
</Dropdown>
@ -142,11 +169,17 @@ export const Organizations = () => {
secondaryActions={[
{
text: t("joinOrganization"),
onClick: () => alert("join organization"),
onClick: () => {
setShouldJoin(true);
setOpenOrganizationPicker(true);
},
},
{
text: t("sendInvitation"),
onClick: () => alert("send invitation"),
onClick: () => {
setShouldJoin(false);
setOpenOrganizationPicker(true);
},
},
]}
/>

View file

@ -111,6 +111,15 @@ export class Organizations extends Resource<{ realm?: string }> {
urlParamKeys: ["orgId"],
});
public inviteExistingUser = this.makeUpdateRequest<
{ orgId: string },
FormData
>({
method: "POST",
path: "/{orgId}/members/invite-existing-user",
urlParamKeys: ["orgId"],
});
public listIdentityProviders = this.makeRequest<
{ orgId: string },
IdentityProviderRepresentation[]