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 joinOrganization=Join organization
sendInvitation=Send invitation sendInvitation=Send invitation
removeConfirmOrganizationTitle=Remove organization? 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 organizationRemovedSuccess=User removed from organizations
organizationRemoveError=Could not remove user from organizations\: {{error}} organizationRemoveError=Could not remove user from organizations\: {{error}}
organizationName=Organization name organizationName=Organization name
@ -3231,3 +3232,4 @@ join=Join
userAddedOrganization_one=Organization added to the user userAddedOrganization_one=Organization added to the user
userAddedOrganizationError=Could not add organizations to the user\: {{error}} userAddedOrganizationError=Could not add organizations to the user\: {{error}}
userAddedOrganization_other={{count}} organizations added to the user 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 OrganizationRepresentation from "@keycloak/keycloak-admin-client/lib/defs/organizationRepresentation";
import UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation"; import UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation";
import { Button, Modal, ModalVariant } from "@patternfly/react-core"; import { Button, Modal, ModalVariant } from "@patternfly/react-core";
import { differenceBy } from "lodash-es";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useAdminClient } from "../admin-client"; import { useAdminClient } from "../admin-client";
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable"; import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
import { TableText } from "@patternfly/react-table";
type OrganizationModalProps = { type OrganizationModalProps = {
isJoin?: boolean;
existingOrgs: OrganizationRepresentation[];
onAdd: (orgs: OrganizationRepresentation[]) => Promise<void>; onAdd: (orgs: OrganizationRepresentation[]) => Promise<void>;
onClose: () => void; onClose: () => void;
}; };
export const OrganizationModal = ({ export const OrganizationModal = ({
isJoin = true,
existingOrgs,
onAdd, onAdd,
onClose, onClose,
}: OrganizationModalProps) => { }: OrganizationModalProps) => {
@ -21,13 +27,20 @@ export const OrganizationModal = ({
const [selectedRows, setSelectedRows] = useState<UserRepresentation[]>([]); const [selectedRows, setSelectedRows] = useState<UserRepresentation[]>([]);
const loader = async (first?: number, max?: number, search?: string) => { 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 ( return (
<Modal <Modal
variant={ModalVariant.small} variant={ModalVariant.small}
title={t("joinOrganization")} title={isJoin ? t("joinOrganization") : t("sendInvitation")}
isOpen isOpen
onClose={onClose} onClose={onClose}
actions={[ actions={[
@ -40,7 +53,7 @@ export const OrganizationModal = ({
onClose(); onClose();
}} }}
> >
{t("join")} {isJoin ? t("join") : t("send")}
</Button>, </Button>,
<Button <Button
data-testid="cancel" data-testid="cancel"
@ -66,6 +79,9 @@ export const OrganizationModal = ({
}, },
{ {
name: "description", name: "description",
cellRenderer: (row) => (
<TableText wrapModifier="truncate">{row.description}</TableText>
),
}, },
]} ]}
/> />

View file

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