Split out the key based into a sperate component (#2177)
This commit is contained in:
parent
d45e53f350
commit
8d5b2f903a
5 changed files with 303 additions and 204 deletions
|
@ -29,7 +29,7 @@ import type RoleRepresentation from "@keycloak/keycloak-admin-client/lib/defs/ro
|
|||
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
|
||||
import type ResourceEvaluation from "@keycloak/keycloak-admin-client/lib/defs/resourceEvaluation";
|
||||
import { useRealm } from "../../context/realm-context/RealmContext";
|
||||
import { AttributeInput } from "../../components/attribute-input/AttributeInput";
|
||||
import { KeyBasedAttributeInput } from "./KeyBasedAttributeInput";
|
||||
import { defaultContextAttributes } from "../utils";
|
||||
import type EvaluationResultRepresentation from "@keycloak/keycloak-admin-client/lib/defs/evaluationResultRepresentation";
|
||||
import type ResourceRepresentation from "@keycloak/keycloak-admin-client/lib/defs/resourceRepresentation";
|
||||
|
@ -518,13 +518,12 @@ export const AuthorizationEvaluate = ({ client }: Props) => {
|
|||
helperTextInvalid={t("common:required")}
|
||||
fieldId="resourcesAndAuthScopes"
|
||||
>
|
||||
<AttributeInput
|
||||
<KeyBasedAttributeInput
|
||||
selectableValues={resources.map<AttributeType>((item) => ({
|
||||
name: item.name!,
|
||||
key: item._id!,
|
||||
}))}
|
||||
resources={resources}
|
||||
isKeySelectable
|
||||
name="resources"
|
||||
/>
|
||||
</FormGroup>
|
||||
|
@ -613,9 +612,8 @@ export const AuthorizationEvaluate = ({ client }: Props) => {
|
|||
helperTextInvalid={t("common:required")}
|
||||
fieldId="contextualAttributes"
|
||||
>
|
||||
<AttributeInput
|
||||
<KeyBasedAttributeInput
|
||||
selectableValues={defaultContextAttributes}
|
||||
isKeySelectable
|
||||
name="context.attributes"
|
||||
/>
|
||||
</FormGroup>
|
||||
|
|
271
src/clients/authorization/KeyBasedAttributeInput.tsx
Normal file
271
src/clients/authorization/KeyBasedAttributeInput.tsx
Normal file
|
@ -0,0 +1,271 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
|
||||
import {
|
||||
Button,
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
TextInput,
|
||||
} from "@patternfly/react-core";
|
||||
import {
|
||||
TableComposable,
|
||||
Tbody,
|
||||
Td,
|
||||
Th,
|
||||
Thead,
|
||||
Tr,
|
||||
} from "@patternfly/react-table";
|
||||
import { MinusCircleIcon, PlusCircleIcon } from "@patternfly/react-icons";
|
||||
|
||||
import type ResourceRepresentation from "@keycloak/keycloak-admin-client/lib/defs/resourceRepresentation";
|
||||
import { defaultContextAttributes } from "../utils";
|
||||
import { camelCase } from "lodash-es";
|
||||
|
||||
import "../../components/attribute-form/attribute-form.css";
|
||||
|
||||
export type AttributeType = {
|
||||
key?: string;
|
||||
name: string;
|
||||
custom?: boolean;
|
||||
values?: {
|
||||
[key: string]: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
type AttributeInputProps = {
|
||||
name: string;
|
||||
selectableValues?: AttributeType[];
|
||||
resources?: ResourceRepresentation[];
|
||||
};
|
||||
|
||||
type ValueInputProps = {
|
||||
name: string;
|
||||
rowIndex: number;
|
||||
attribute: any;
|
||||
selectableValues?: AttributeType[];
|
||||
resources?: ResourceRepresentation[];
|
||||
};
|
||||
|
||||
const ValueInput = ({
|
||||
name,
|
||||
rowIndex,
|
||||
attribute,
|
||||
selectableValues,
|
||||
resources,
|
||||
}: ValueInputProps) => {
|
||||
const { t } = useTranslation("common");
|
||||
const { control, register, getValues } = useFormContext();
|
||||
|
||||
const [isValueOpenArray, setIsValueOpenArray] = useState([false]);
|
||||
|
||||
const toggleValueSelect = (rowIndex: number, open: boolean) => {
|
||||
const arr = [...isValueOpenArray];
|
||||
arr[rowIndex] = open;
|
||||
setIsValueOpenArray(arr);
|
||||
};
|
||||
|
||||
let attributeValues: { key: string; name: string }[] | undefined = [];
|
||||
|
||||
const scopeValues = resources?.find(
|
||||
(resource) => resource.name === getValues().resources[rowIndex]?.key
|
||||
)?.scopes;
|
||||
|
||||
if (selectableValues) {
|
||||
attributeValues = defaultContextAttributes.find(
|
||||
(attr) => attr.key === getValues().context[rowIndex]?.key
|
||||
)?.values;
|
||||
}
|
||||
|
||||
const renderSelectOptionType = () => {
|
||||
if (attributeValues?.length && !resources) {
|
||||
return attributeValues.map((attr) => (
|
||||
<SelectOption key={attr.key} value={attr.key}>
|
||||
{attr.name}
|
||||
</SelectOption>
|
||||
));
|
||||
} else if (scopeValues?.length) {
|
||||
return scopeValues.map((scope) => (
|
||||
<SelectOption key={scope.name} value={scope.name}>
|
||||
{scope.name}
|
||||
</SelectOption>
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
const getMessageBundleKey = (attributeName: string) =>
|
||||
camelCase(attributeName).replace(/\W/g, "");
|
||||
|
||||
return (
|
||||
<Td>
|
||||
{resources || attributeValues?.length ? (
|
||||
<Controller
|
||||
name={`${name}[${rowIndex}].value`}
|
||||
defaultValue={[]}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Select
|
||||
id={`${attribute.id}-value`}
|
||||
className="kc-attribute-value-selectable"
|
||||
name={`${name}[${rowIndex}].value`}
|
||||
chipGroupProps={{
|
||||
numChips: 1,
|
||||
expandedText: t("common:hide"),
|
||||
collapsedText: t("common:showRemaining"),
|
||||
}}
|
||||
toggleId={`group-${name}`}
|
||||
onToggle={(open) => toggleValueSelect(rowIndex, open)}
|
||||
isOpen={isValueOpenArray[rowIndex]}
|
||||
variant={SelectVariant.typeahead}
|
||||
typeAheadAriaLabel={t("clients:selectOrTypeAKey")}
|
||||
placeholderText={t("clients:selectOrTypeAKey")}
|
||||
selections={value}
|
||||
onSelect={(_, v) => {
|
||||
onChange(v);
|
||||
|
||||
toggleValueSelect(rowIndex, false);
|
||||
}}
|
||||
>
|
||||
{renderSelectOptionType()}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<TextInput
|
||||
id={`${getMessageBundleKey(attribute.key)}-value`}
|
||||
className="value-input"
|
||||
name={`${name}[${rowIndex}].value`}
|
||||
ref={register()}
|
||||
defaultValue={attribute.value}
|
||||
data-testid="attribute-value-input"
|
||||
/>
|
||||
)}
|
||||
</Td>
|
||||
);
|
||||
};
|
||||
|
||||
export const KeyBasedAttributeInput = ({
|
||||
name,
|
||||
selectableValues,
|
||||
resources,
|
||||
}: AttributeInputProps) => {
|
||||
const { t } = useTranslation("common");
|
||||
const { control, watch } = useFormContext();
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control: control,
|
||||
name,
|
||||
});
|
||||
|
||||
const [isKeyOpenArray, setIsKeyOpenArray] = useState([false]);
|
||||
const toggleKeySelect = (rowIndex: number, open: boolean) => {
|
||||
const arr = [...isKeyOpenArray];
|
||||
arr[rowIndex] = open;
|
||||
setIsKeyOpenArray(arr);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!fields.length) {
|
||||
append({ key: "", value: "" });
|
||||
}
|
||||
}, [fields]);
|
||||
|
||||
const watchLastValue = watch(`${name}[${fields.length - 1}].value`, "");
|
||||
|
||||
return (
|
||||
<TableComposable
|
||||
className="kc-attributes__table"
|
||||
aria-label="Role attribute keys and values"
|
||||
variant="compact"
|
||||
borders={false}
|
||||
>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th id="key" width={40}>
|
||||
{t("key")}
|
||||
</Th>
|
||||
<Th id="value" width={40}>
|
||||
{t("value")}
|
||||
</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{fields.map((attribute, rowIndex) => (
|
||||
<Tr key={attribute.id} data-testid="attribute-row">
|
||||
<Td>
|
||||
<Controller
|
||||
name={`${name}[${rowIndex}].key`}
|
||||
defaultValue={attribute.key}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Select
|
||||
id={`${name}[${rowIndex}].key`}
|
||||
className="kc-attribute-key-selectable"
|
||||
name={`${name}[${rowIndex}].key`}
|
||||
toggleId={`group-${name}`}
|
||||
onToggle={(open) => toggleKeySelect(rowIndex, open)}
|
||||
isOpen={isKeyOpenArray[rowIndex]}
|
||||
variant={SelectVariant.typeahead}
|
||||
typeAheadAriaLabel={t("clients:selectOrTypeAKey")}
|
||||
placeholderText={t("clients:selectOrTypeAKey")}
|
||||
selections={value}
|
||||
onSelect={(_, v) => {
|
||||
onChange(v.toString());
|
||||
|
||||
toggleKeySelect(rowIndex, false);
|
||||
}}
|
||||
>
|
||||
{selectableValues?.map((attribute) => (
|
||||
<SelectOption
|
||||
selected={attribute.name === value}
|
||||
key={attribute.key}
|
||||
value={resources ? attribute.name : attribute.key}
|
||||
>
|
||||
{attribute.name}
|
||||
</SelectOption>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</Td>
|
||||
<ValueInput
|
||||
name={name}
|
||||
attribute={attribute}
|
||||
rowIndex={rowIndex}
|
||||
selectableValues={selectableValues}
|
||||
resources={resources}
|
||||
/>
|
||||
<Td key="minus-button" id={`kc-minus-button-${rowIndex}`}>
|
||||
<Button
|
||||
id={`minus-button-${rowIndex}`}
|
||||
variant="link"
|
||||
className="kc-attributes__minus-icon"
|
||||
onClick={() => remove(rowIndex)}
|
||||
>
|
||||
<MinusCircleIcon />
|
||||
</Button>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
<Tr>
|
||||
<Td>
|
||||
<Button
|
||||
aria-label={t("roles:addAttributeText")}
|
||||
id="plus-icon"
|
||||
variant="link"
|
||||
className="kc-attributes__plus-icon"
|
||||
onClick={() => {
|
||||
append({ key: "", value: "" });
|
||||
setIsKeyOpenArray([...isKeyOpenArray, false]);
|
||||
}}
|
||||
icon={<PlusCircleIcon />}
|
||||
isDisabled={!watchLastValue}
|
||||
data-testid="attribute-add-row"
|
||||
>
|
||||
{t("roles:addAttributeText")}
|
||||
</Button>
|
||||
</Td>
|
||||
</Tr>
|
||||
</Tbody>
|
||||
</TableComposable>
|
||||
);
|
||||
};
|
|
@ -5,10 +5,7 @@ import { ActionGroup, Button } from "@patternfly/react-core";
|
|||
|
||||
import type { RoleRepresentation } from "../../model/role-model";
|
||||
import type { KeyValueType } from "./attribute-convert";
|
||||
import {
|
||||
AttributeInput,
|
||||
AttributeType,
|
||||
} from "../attribute-input/AttributeInput";
|
||||
import { AttributeInput } from "../attribute-input/AttributeInput";
|
||||
import { FormAccess } from "../form-access/FormAccess";
|
||||
|
||||
export type AttributeForm = Omit<RoleRepresentation, "attributes"> & {
|
||||
|
@ -17,19 +14,11 @@ export type AttributeForm = Omit<RoleRepresentation, "attributes"> & {
|
|||
|
||||
export type AttributesFormProps = {
|
||||
form: UseFormMethods<AttributeForm>;
|
||||
isKeySelectable?: boolean;
|
||||
selectableValues?: AttributeType[];
|
||||
save?: (model: AttributeForm) => void;
|
||||
reset?: () => void;
|
||||
};
|
||||
|
||||
export const AttributesForm = ({
|
||||
form,
|
||||
reset,
|
||||
save,
|
||||
isKeySelectable,
|
||||
selectableValues,
|
||||
}: AttributesFormProps) => {
|
||||
export const AttributesForm = ({ form, reset, save }: AttributesFormProps) => {
|
||||
const { t } = useTranslation("roles");
|
||||
const noSaveCancelButtons = !save && !reset;
|
||||
const {
|
||||
|
@ -43,11 +32,7 @@ export const AttributesForm = ({
|
|||
onSubmit={save ? handleSubmit(save) : undefined}
|
||||
>
|
||||
<FormProvider {...form}>
|
||||
<AttributeInput
|
||||
isKeySelectable={isKeySelectable}
|
||||
selectableValues={selectableValues}
|
||||
name="attributes"
|
||||
/>
|
||||
<AttributeInput name="attributes" />
|
||||
</FormProvider>
|
||||
{!noSaveCancelButtons && (
|
||||
<ActionGroup className="kc-attributes__action-group">
|
||||
|
|
|
@ -1,13 +1,7 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
|
||||
import {
|
||||
Button,
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
TextInput,
|
||||
} from "@patternfly/react-core";
|
||||
import { useFieldArray, useFormContext } from "react-hook-form";
|
||||
import { Button, TextInput } from "@patternfly/react-core";
|
||||
import {
|
||||
TableComposable,
|
||||
Tbody,
|
||||
|
@ -19,142 +13,27 @@ import {
|
|||
import { MinusCircleIcon, PlusCircleIcon } from "@patternfly/react-icons";
|
||||
|
||||
import "../attribute-form/attribute-form.css";
|
||||
import { defaultContextAttributes } from "../../clients/utils";
|
||||
import { camelCase } from "lodash-es";
|
||||
import type ResourceRepresentation from "@keycloak/keycloak-admin-client/lib/defs/resourceRepresentation";
|
||||
|
||||
export type AttributeType = {
|
||||
key?: string;
|
||||
name: string;
|
||||
custom?: boolean;
|
||||
values?: {
|
||||
[key: string]: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
type AttributeInputProps = {
|
||||
name: string;
|
||||
selectableValues?: AttributeType[];
|
||||
isKeySelectable?: boolean;
|
||||
resources?: ResourceRepresentation[];
|
||||
};
|
||||
|
||||
export const AttributeInput = ({
|
||||
name,
|
||||
isKeySelectable,
|
||||
selectableValues,
|
||||
resources,
|
||||
}: AttributeInputProps) => {
|
||||
export const AttributeInput = ({ name }: AttributeInputProps) => {
|
||||
const { t } = useTranslation("common");
|
||||
const { control, register, watch, getValues } = useFormContext();
|
||||
const { control, register, watch } = useFormContext();
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control: control,
|
||||
name,
|
||||
});
|
||||
|
||||
const watchLast = watch(`${name}[${fields.length - 1}].key`, "");
|
||||
|
||||
useEffect(() => {
|
||||
if (!fields.length) {
|
||||
append({ key: "", value: "" });
|
||||
}
|
||||
}, [fields]);
|
||||
|
||||
const [isKeyOpenArray, setIsKeyOpenArray] = useState([false]);
|
||||
const watchLastKey = watch(`${name}[${fields.length - 1}].key`, "");
|
||||
const watchLastValue = watch(`${name}[${fields.length - 1}].value`, "");
|
||||
|
||||
const [isValueOpenArray, setIsValueOpenArray] = useState([false]);
|
||||
const toggleKeySelect = (rowIndex: number, open: boolean) => {
|
||||
const arr = [...isKeyOpenArray];
|
||||
arr[rowIndex] = open;
|
||||
setIsKeyOpenArray(arr);
|
||||
};
|
||||
|
||||
const toggleValueSelect = (rowIndex: number, open: boolean) => {
|
||||
const arr = [...isValueOpenArray];
|
||||
arr[rowIndex] = open;
|
||||
setIsValueOpenArray(arr);
|
||||
};
|
||||
|
||||
const renderValueInput = (rowIndex: number, attribute: any) => {
|
||||
let attributeValues: { key: string; name: string }[] | undefined = [];
|
||||
|
||||
const scopeValues = resources?.find(
|
||||
(resource) => resource.name === getValues().resources[rowIndex]?.key
|
||||
)?.scopes;
|
||||
|
||||
if (selectableValues) {
|
||||
attributeValues = defaultContextAttributes.find(
|
||||
(attr) => attr.key === getValues().context[rowIndex]?.key
|
||||
)?.values;
|
||||
}
|
||||
|
||||
const renderSelectOptionType = () => {
|
||||
if (attributeValues?.length && !resources) {
|
||||
return attributeValues.map((attr) => (
|
||||
<SelectOption key={attr.key} value={attr.key}>
|
||||
{attr.name}
|
||||
</SelectOption>
|
||||
));
|
||||
} else if (scopeValues?.length) {
|
||||
return scopeValues.map((scope) => (
|
||||
<SelectOption key={scope.name} value={scope.name}>
|
||||
{scope.name}
|
||||
</SelectOption>
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
const getMessageBundleKey = (attributeName: string) =>
|
||||
camelCase(attributeName).replace(/\W/g, "");
|
||||
|
||||
return (
|
||||
<Td>
|
||||
{resources || attributeValues?.length ? (
|
||||
<Controller
|
||||
name={`${name}[${rowIndex}].value`}
|
||||
defaultValue={[]}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Select
|
||||
id={`${attribute.id}-value`}
|
||||
className="kc-attribute-value-selectable"
|
||||
name={`${name}[${rowIndex}].value`}
|
||||
chipGroupProps={{
|
||||
numChips: 1,
|
||||
expandedText: t("common:hide"),
|
||||
collapsedText: t("common:showRemaining"),
|
||||
}}
|
||||
toggleId={`group-${name}`}
|
||||
onToggle={(open) => toggleValueSelect(rowIndex, open)}
|
||||
isOpen={isValueOpenArray[rowIndex]}
|
||||
variant={SelectVariant.typeahead}
|
||||
typeAheadAriaLabel={t("clients:selectOrTypeAKey")}
|
||||
placeholderText={t("clients:selectOrTypeAKey")}
|
||||
selections={value}
|
||||
onSelect={(_, v) => {
|
||||
onChange(v);
|
||||
|
||||
toggleValueSelect(rowIndex, false);
|
||||
}}
|
||||
>
|
||||
{renderSelectOptionType()}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<TextInput
|
||||
id={`${getMessageBundleKey(attribute.key)}-value`}
|
||||
className="value-input"
|
||||
name={`${name}[${rowIndex}].value`}
|
||||
ref={register()}
|
||||
defaultValue={attribute.value}
|
||||
data-testid="attribute-value-input"
|
||||
/>
|
||||
)}
|
||||
</Td>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<TableComposable
|
||||
className="kc-attributes__table"
|
||||
|
@ -176,42 +55,6 @@ export const AttributeInput = ({
|
|||
{fields.map((attribute, rowIndex) => (
|
||||
<Tr key={attribute.id} data-testid="attribute-row">
|
||||
<Td>
|
||||
{isKeySelectable ? (
|
||||
<Controller
|
||||
name={`${name}[${rowIndex}].key`}
|
||||
defaultValue={attribute.key}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Select
|
||||
id={`${name}[${rowIndex}].key`}
|
||||
className="kc-attribute-key-selectable"
|
||||
name={`${name}[${rowIndex}].key`}
|
||||
toggleId={`group-${name}`}
|
||||
onToggle={(open) => toggleKeySelect(rowIndex, open)}
|
||||
isOpen={isKeyOpenArray[rowIndex]}
|
||||
variant={SelectVariant.typeahead}
|
||||
typeAheadAriaLabel={t("clients:selectOrTypeAKey")}
|
||||
placeholderText={t("clients:selectOrTypeAKey")}
|
||||
selections={value}
|
||||
onSelect={(_, v) => {
|
||||
onChange(v.toString());
|
||||
|
||||
toggleKeySelect(rowIndex, false);
|
||||
}}
|
||||
>
|
||||
{selectableValues?.map((attribute) => (
|
||||
<SelectOption
|
||||
selected={attribute.name === value}
|
||||
key={attribute.key}
|
||||
value={resources ? attribute.name : attribute.key}
|
||||
>
|
||||
{attribute.name}
|
||||
</SelectOption>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<TextInput
|
||||
id={`${attribute.id}-key`}
|
||||
name={`${name}[${rowIndex}].key`}
|
||||
|
@ -219,9 +62,16 @@ export const AttributeInput = ({
|
|||
defaultValue={attribute.key}
|
||||
data-testid="attribute-key-input"
|
||||
/>
|
||||
)}
|
||||
</Td>
|
||||
{renderValueInput(rowIndex, attribute)}
|
||||
<Td>
|
||||
<TextInput
|
||||
id={`${attribute.id}-value`}
|
||||
name={`${name}[${rowIndex}].value`}
|
||||
ref={register()}
|
||||
defaultValue={attribute.value}
|
||||
data-testid="attribute-value-input"
|
||||
/>
|
||||
</Td>
|
||||
<Td key="minus-button" id={`kc-minus-button-${rowIndex}`}>
|
||||
<Button
|
||||
id={`minus-button-${rowIndex}`}
|
||||
|
@ -241,14 +91,9 @@ export const AttributeInput = ({
|
|||
id="plus-icon"
|
||||
variant="link"
|
||||
className="kc-attributes__plus-icon"
|
||||
onClick={() => {
|
||||
append({ key: "", value: "" });
|
||||
if (isKeySelectable) {
|
||||
setIsKeyOpenArray([...isKeyOpenArray, false]);
|
||||
}
|
||||
}}
|
||||
onClick={() => append({ key: "", value: "" })}
|
||||
icon={<PlusCircleIcon />}
|
||||
isDisabled={isKeySelectable ? !watchLastValue : !watchLastKey}
|
||||
isDisabled={!watchLast}
|
||||
data-testid="attribute-add-row"
|
||||
>
|
||||
{t("roles:addAttributeText")}
|
||||
|
|
|
@ -182,7 +182,7 @@ export default function RealmRoleTabs() {
|
|||
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
||||
titleKey: "roles:roleDeleteConfirm",
|
||||
messageKey: t("roles:roleDeleteConfirmDialog", {
|
||||
selectedRoleName: role?.name || t("createRole"),
|
||||
name: role?.name || t("createRole"),
|
||||
}),
|
||||
continueButtonLabel: "common:delete",
|
||||
continueButtonVariant: ButtonVariant.danger,
|
||||
|
|
Loading…
Reference in a new issue