parent
7c50b10467
commit
cd732ae44f
3 changed files with 138 additions and 48 deletions
|
@ -19,6 +19,7 @@ export default class PermissionsTab extends CommonPage {
|
|||
.parent()
|
||||
.parent()
|
||||
.findByText(name)
|
||||
.parent()
|
||||
.click();
|
||||
return this;
|
||||
}
|
||||
|
|
|
@ -543,5 +543,7 @@
|
|||
"never": "Never expires"
|
||||
},
|
||||
"mappers": "Mappers",
|
||||
"sessions": "Sessions"
|
||||
"sessions": "Sessions",
|
||||
"unsavedChangesTitle": "Unsaved changes",
|
||||
"unsavedChangesConfirm": "You have unsaved changes. Do you really want to leave the page?"
|
||||
}
|
||||
|
|
|
@ -4,13 +4,28 @@ import type {
|
|||
Clients,
|
||||
PolicyQuery,
|
||||
} from "@keycloak/keycloak-admin-client/lib/resources/clients";
|
||||
import { Select, SelectOption, SelectVariant } from "@patternfly/react-core";
|
||||
import {
|
||||
ButtonVariant,
|
||||
Chip,
|
||||
ChipGroup,
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
} from "@patternfly/react-core";
|
||||
import { useState } from "react";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import {
|
||||
Controller,
|
||||
ControllerRenderProps,
|
||||
useFormContext,
|
||||
} from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { adminClient } from "../../admin-client";
|
||||
import { useRealm } from "../../context/realm-context/RealmContext";
|
||||
import { useFetch } from "../../utils/useFetch";
|
||||
import { toPolicyDetails } from "../routes/PolicyDetails";
|
||||
import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog";
|
||||
|
||||
type Type = "resources" | "policies";
|
||||
|
||||
|
@ -26,6 +41,7 @@ type ResourcesPolicySelectProps = {
|
|||
type Policies = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
type?: string;
|
||||
};
|
||||
|
||||
type TypeMapping = {
|
||||
|
@ -57,15 +73,18 @@ export const ResourcesPolicySelect = ({
|
|||
preSelected,
|
||||
isRequired = false,
|
||||
}: ResourcesPolicySelectProps) => {
|
||||
const { realm } = useRealm();
|
||||
const { t } = useTranslation("clients");
|
||||
const navigate = useNavigate();
|
||||
|
||||
const {
|
||||
control,
|
||||
formState: { errors },
|
||||
formState: { errors, isDirty },
|
||||
} = useFormContext<PolicyRepresentation>();
|
||||
const [items, setItems] = useState<Policies[]>([]);
|
||||
const [search, setSearch] = useState("");
|
||||
const [open, setOpen] = useState(false);
|
||||
const [clickedPolicy, setClickedPolicy] = useState<Policies>();
|
||||
|
||||
const functions = typeMapping[name];
|
||||
|
||||
|
@ -74,6 +93,7 @@ export const ResourcesPolicySelect = ({
|
|||
): Policies => ({
|
||||
id: "_id" in p ? p._id : "id" in p ? p.id : undefined,
|
||||
name: p.name,
|
||||
type: p.type,
|
||||
});
|
||||
|
||||
useFetch(
|
||||
|
@ -108,6 +128,22 @@ export const ResourcesPolicySelect = ({
|
|||
[search],
|
||||
);
|
||||
|
||||
const [toggleUnsavedChangesDialog, UnsavedChangesConfirm] = useConfirmDialog({
|
||||
titleKey: t("unsavedChangesTitle"),
|
||||
messageKey: t("unsavedChangesConfirm"),
|
||||
continueButtonLabel: t("common:continue"),
|
||||
continueButtonVariant: ButtonVariant.danger,
|
||||
onConfirm: () => navigate(to(clickedPolicy!)),
|
||||
});
|
||||
|
||||
const to = (policy: Policies) =>
|
||||
toPolicyDetails({
|
||||
realm: realm,
|
||||
id: clientId,
|
||||
policyId: policy.id!,
|
||||
policyType: policy.type!,
|
||||
});
|
||||
|
||||
const toSelectOptions = () =>
|
||||
items.map((p) => (
|
||||
<SelectOption key={p.id} value={p.id}>
|
||||
|
@ -115,50 +151,101 @@ export const ResourcesPolicySelect = ({
|
|||
</SelectOption>
|
||||
));
|
||||
|
||||
return (
|
||||
<Controller
|
||||
name={name}
|
||||
defaultValue={preSelected ? [preSelected] : []}
|
||||
control={control}
|
||||
rules={{ validate: (value) => !isRequired || value!.length > 0 }}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
toggleId={name}
|
||||
variant={variant}
|
||||
onToggle={setOpen}
|
||||
onFilter={(_, filter) => {
|
||||
setSearch(filter);
|
||||
return toSelectOptions();
|
||||
}}
|
||||
onClear={() => {
|
||||
field.onChange([]);
|
||||
setSearch("");
|
||||
}}
|
||||
selections={field.value}
|
||||
onSelect={(_, selectedValue) => {
|
||||
const option = selectedValue.toString();
|
||||
if (variant === SelectVariant.typeaheadMulti) {
|
||||
const changedValue = field.value?.find(
|
||||
(p: string) => p === option,
|
||||
)
|
||||
? field.value.filter((p: string) => p !== option)
|
||||
: [...field.value!, option];
|
||||
field.onChange(changedValue);
|
||||
} else {
|
||||
field.onChange([option]);
|
||||
}
|
||||
const toChipGroupItems = (
|
||||
field: ControllerRenderProps<PolicyRepresentation, Type>,
|
||||
) => {
|
||||
return (
|
||||
<ChipGroup>
|
||||
{field.value?.map((permissionId) => {
|
||||
const policy = items.find(
|
||||
(permission) => permission.id === permissionId,
|
||||
);
|
||||
|
||||
setSearch("");
|
||||
}}
|
||||
isOpen={open}
|
||||
aria-labelledby={t(name)}
|
||||
isDisabled={!!preSelected}
|
||||
validated={errors[name] ? "error" : "default"}
|
||||
typeAheadAriaLabel={t(name)}
|
||||
>
|
||||
{toSelectOptions()}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
if (!policy) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Chip
|
||||
key={policy.id}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
field.onChange(field.value?.filter((id) => id !== policy.id));
|
||||
}}
|
||||
>
|
||||
{policy.type ? (
|
||||
<Link
|
||||
to={to(policy)}
|
||||
onClick={(event) => {
|
||||
if (isDirty) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setOpen(false);
|
||||
setClickedPolicy(policy);
|
||||
toggleUnsavedChangesDialog();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{policy.name}
|
||||
</Link>
|
||||
) : (
|
||||
policy.name
|
||||
)}
|
||||
</Chip>
|
||||
);
|
||||
})}
|
||||
</ChipGroup>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<UnsavedChangesConfirm />
|
||||
<Controller
|
||||
name={name}
|
||||
defaultValue={preSelected ? [preSelected] : []}
|
||||
control={control}
|
||||
rules={{ validate: (value) => !isRequired || value!.length > 0 }}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
toggleId={name}
|
||||
variant={variant}
|
||||
onToggle={setOpen}
|
||||
onFilter={(_, filter) => {
|
||||
setSearch(filter);
|
||||
return toSelectOptions();
|
||||
}}
|
||||
onClear={() => {
|
||||
field.onChange([]);
|
||||
setSearch("");
|
||||
}}
|
||||
selections={field.value}
|
||||
onSelect={(_, selectedValue) => {
|
||||
const option = selectedValue.toString();
|
||||
if (variant === SelectVariant.typeaheadMulti) {
|
||||
const changedValue = field.value?.find(
|
||||
(p: string) => p === option,
|
||||
)
|
||||
? field.value.filter((p: string) => p !== option)
|
||||
: [...field.value!, option];
|
||||
field.onChange(changedValue);
|
||||
} else {
|
||||
field.onChange([option]);
|
||||
}
|
||||
|
||||
setSearch("");
|
||||
}}
|
||||
isOpen={open}
|
||||
aria-labelledby={t(name)}
|
||||
isDisabled={!!preSelected}
|
||||
validated={errors[name] ? "error" : "default"}
|
||||
typeAheadAriaLabel={t(name)}
|
||||
chipGroupComponent={toChipGroupItems(field)}
|
||||
>
|
||||
{toSelectOptions()}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue