added join org modal

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
This commit is contained in:
Erik Jan de Wit 2024-08-16 11:26:42 +02:00 committed by Pedro Igor
parent a3ffbb439d
commit e6dd8ac1c0
5 changed files with 240 additions and 59 deletions

View file

@ -3221,3 +3221,13 @@ emptyUserOrganizations=No organizations
emptyUserOrganizationsInstructions=There is no organization yet. Please join an organization or send an invitation to join an organization.
joinOrganization=Join organization
sendInvitation=Send invitation
removeConfirmOrganizationTitle=Remove organization?
organizationRemoveConfirm=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
joinOrganization=Join organization
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

View file

@ -0,0 +1,74 @@
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 { useState } from "react";
import { useTranslation } from "react-i18next";
import { useAdminClient } from "../admin-client";
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
type OrganizationModalProps = {
onAdd: (orgs: OrganizationRepresentation[]) => Promise<void>;
onClose: () => void;
};
export const OrganizationModal = ({
onAdd,
onClose,
}: OrganizationModalProps) => {
const { adminClient } = useAdminClient();
const { t } = useTranslation();
const [selectedRows, setSelectedRows] = useState<UserRepresentation[]>([]);
const loader = async (first?: number, max?: number, search?: string) => {
return await adminClient.organizations.find({ first, max, search });
};
return (
<Modal
variant={ModalVariant.small}
title={t("joinOrganization")}
isOpen
onClose={onClose}
actions={[
<Button
data-testid="join"
key="confirm"
variant="primary"
onClick={async () => {
await onAdd(selectedRows);
onClose();
}}
>
{t("join")}
</Button>,
<Button
data-testid="cancel"
key="cancel"
variant="link"
onClick={onClose}
>
{t("cancel")}
</Button>,
]}
>
<KeycloakDataTable
loader={loader}
isPaginated
ariaLabelKey="organizationsList"
searchPlaceholderKey="searchOrganization"
canSelectAll
onSelect={(rows) => setSelectedRows([...rows])}
columns={[
{
name: "name",
displayKey: "organizationName",
},
{
name: "description",
},
]}
/>
</Modal>
);
};

View file

@ -57,17 +57,21 @@ const Domains = (org: OrganizationRepresentation) => {
};
type OrganizationTableProps = PropsWithChildren & {
loader:
| LoaderFunction<OrganizationRepresentation>
| OrganizationRepresentation[];
onDelete?: (org: OrganizationRepresentation) => void;
loader: LoaderFunction<OrganizationRepresentation>;
toolbarItem?: ReactNode;
isPaginated?: boolean;
onSelect?: (orgs: OrganizationRepresentation[]) => void;
onDelete?: (org: OrganizationRepresentation) => void;
deleteLabel?: string;
};
export const OrganizationTable = ({
loader,
toolbarItem,
isPaginated = false,
onSelect,
onDelete,
deleteLabel = "delete",
children,
}: OrganizationTableProps) => {
const { t } = useTranslation();
@ -75,13 +79,15 @@ export const OrganizationTable = ({
return (
<KeycloakDataTable
loader={loader}
isPaginated
isPaginated={isPaginated}
ariaLabelKey="organizationList"
searchPlaceholderKey="searchOrganization"
toolbarItem={toolbarItem}
onSelect={onSelect}
canSelectAll={onSelect !== undefined}
actions={[
{
title: t("delete"),
title: t(deleteLabel),
onRowClick: onDelete,
},
]}

View file

@ -63,6 +63,7 @@ export default function OrganizationSection() {
<OrganizationTable
key={key}
loader={loader}
isPaginated
toolbarItem={
<ToolbarItem>
<Button

View file

@ -1,66 +1,156 @@
import { Button, ToolbarItem } from "@patternfly/react-core";
import OrganizationRepresentation from "@keycloak/keycloak-admin-client/lib/defs/organizationRepresentation";
import { useAlerts } from "@keycloak/keycloak-ui-shared";
import {
Button,
ButtonVariant,
Dropdown,
DropdownItem,
DropdownList,
MenuToggle,
ToolbarItem,
} from "@patternfly/react-core";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link, useParams } from "react-router-dom";
import { useParams } from "react-router-dom";
import { useAdminClient } from "../admin-client";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
import { useRealm } from "../context/realm-context/RealmContext";
import { OrganizationModal } from "../organizations/OrganizationModal";
import { OrganizationTable } from "../organizations/OrganizationTable";
import { toAddOrganization } from "../organizations/routes/AddOrganization";
import useToggle from "../utils/useToggle";
import { UserParams } from "./routes/User";
export const Organizations = () => {
const { adminClient } = useAdminClient();
const { t } = useTranslation();
const { realm } = useRealm();
const { id } = useParams<UserParams>();
const { addAlert, addError } = useAlerts();
const [key, setKey] = useState(0);
const refresh = () => setKey(key + 1);
const [joinToggle, toggle, setJoinToggle] = useToggle();
const [joinOrganization, setJoinOrganization] = useState(false);
const [selectedOrgs, setSelectedOrgs] = useState<
OrganizationRepresentation[]
>([]);
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
titleKey: "removeConfirmOrganizationTitle",
messageKey: t("organizationRemoveConfirm", { count: selectedOrgs.length }),
continueButtonLabel: "remove",
continueButtonVariant: ButtonVariant.danger,
onConfirm: async () => {
try {
await Promise.all(
selectedOrgs.map((org) =>
adminClient.organizations.delMember({
orgId: org.id!,
userId: id!,
}),
),
);
addAlert(t("organizationRemovedSuccess"));
refresh();
} catch (error) {
addError("organizationRemoveError", error);
}
},
});
return (
<OrganizationTable
loader={() =>
adminClient.organizations.memberOrganizations({
userId: id!,
})
}
toolbarItem={
<>
<ToolbarItem>
<Button
data-testid="joinOrganization"
component={(props) => (
<Link {...props} to={toAddOrganization({ realm })} />
)}
>
{t("joinOrganization")}
</Button>
</ToolbarItem>
<ToolbarItem>
<Button
data-testid="removeOrganization"
variant="secondary"
component={(props) => (
<Link {...props} to={toAddOrganization({ realm })} />
)}
>
{t("remove")}
</Button>
</ToolbarItem>
</>
}
>
<ListEmptyState
message={t("emptyUserOrganizations")}
instructions={t("emptyUserOrganizationsInstructions")}
secondaryActions={[
{
text: t("joinOrganization"),
onClick: () => alert("join organization"),
},
{
text: t("sendInvitation"),
onClick: () => alert("send invitation"),
},
]}
/>
</OrganizationTable>
<>
{joinOrganization && (
<OrganizationModal
onClose={() => setJoinOrganization(false)}
onAdd={async (orgs) => {
try {
await Promise.all(
orgs.map((org) =>
adminClient.organizations.addMember({
orgId: org.id!,
userId: id!,
}),
),
);
addAlert(t("userAddedOrganization", { count: orgs.length }));
refresh();
} catch (error) {
addError("userAddedOrganizationError", error);
}
}}
/>
)}
<DeleteConfirm />
<OrganizationTable
key={key}
loader={() =>
adminClient.organizations.memberOrganizations({
userId: id!,
})
}
onSelect={(orgs) => setSelectedOrgs(orgs)}
deleteLabel="remove"
toolbarItem={
<>
<ToolbarItem>
<Dropdown
onOpenChange={setJoinToggle}
toggle={(ref) => (
<MenuToggle
ref={ref}
id="toggle-id"
onClick={toggle}
variant="primary"
>
{t("joinOrganization")}
</MenuToggle>
)}
isOpen={joinToggle}
>
<DropdownList>
<DropdownItem
key="join"
onClick={() => {
setJoinOrganization(true);
}}
>
{t("joinOrganization")}
</DropdownItem>
<DropdownItem key="invite" component="button">
{t("invite")}
</DropdownItem>
</DropdownList>
</Dropdown>
</ToolbarItem>
<ToolbarItem>
<Button
data-testid="removeOrganization"
variant="secondary"
isDisabled={selectedOrgs.length === 0}
onClick={() => toggleDeleteDialog()}
>
{t("remove")}
</Button>
</ToolbarItem>
</>
}
>
<ListEmptyState
message={t("emptyUserOrganizations")}
instructions={t("emptyUserOrganizationsInstructions")}
secondaryActions={[
{
text: t("joinOrganization"),
onClick: () => alert("join organization"),
},
{
text: t("sendInvitation"),
onClick: () => alert("send invitation"),
},
]}
/>
</OrganizationTable>
</>
);
};