@keycloakify/keycloak-ui-shared 260103.0.1 → 260305.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/keycloak-theme/shared/keycloak-ui-shared/context/ErrorPage.tsx +7 -10
- package/keycloak-theme/shared/keycloak-ui-shared/context/KeycloakContext.tsx +3 -9
- package/keycloak-theme/shared/keycloak-ui-shared/controls/FileUploadControl.tsx +85 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/FormLabel.tsx +4 -2
- package/keycloak-theme/shared/keycloak-ui-shared/controls/OrganizationTable.tsx +3 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/SwitchControl.tsx +3 -2
- package/keycloak-theme/shared/keycloak-ui-shared/controls/TextAreaControl.tsx +1 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/select-control/SelectControl.tsx +5 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/select-control/SingleSelectControl.tsx +16 -5
- package/keycloak-theme/shared/keycloak-ui-shared/controls/select-control/TypeaheadSelectControl.tsx +65 -35
- package/keycloak-theme/shared/keycloak-ui-shared/controls/table/KeycloakDataTable.tsx +56 -44
- package/keycloak-theme/shared/keycloak-ui-shared/controls/table/ListEmptyState.tsx +9 -2
- package/keycloak-theme/shared/keycloak-ui-shared/controls/table/TableToolbar.tsx +1 -1
- package/keycloak-theme/shared/keycloak-ui-shared/main.ts +4 -0
- package/keycloak-theme/shared/keycloak-ui-shared/masthead/Masthead.tsx +3 -0
- package/keycloak-theme/shared/keycloak-ui-shared/scroll-form/ScrollForm.tsx +9 -2
- package/keycloak-theme/shared/keycloak-ui-shared/select/KeycloakSelect.tsx +1 -1
- package/keycloak-theme/shared/keycloak-ui-shared/select/TypeaheadSelect.tsx +5 -1
- package/keycloak-theme/shared/keycloak-ui-shared/user-profile/LocaleSelector.tsx +1 -1
- package/keycloak-theme/shared/keycloak-ui-shared/user-profile/TextComponent.tsx +2 -2
- package/keycloak-theme/shared/keycloak-ui-shared/user-profile/utils.ts +10 -3
- package/package.json +9 -9
|
@@ -11,18 +11,17 @@ import {
|
|
|
11
11
|
TextContent,
|
|
12
12
|
} from "../../@patternfly/react-core";
|
|
13
13
|
import { useTranslation } from "react-i18next";
|
|
14
|
-
import {
|
|
14
|
+
import { getNetworkErrorMessage } from "../utils/errors";
|
|
15
15
|
|
|
16
16
|
type ErrorPageProps = {
|
|
17
17
|
error?: unknown;
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
export const ErrorPage = (props: ErrorPageProps) => {
|
|
21
|
-
const { t } = useTranslation();
|
|
21
|
+
const { t, i18n } = useTranslation();
|
|
22
22
|
const error = props.error;
|
|
23
|
-
const errorMessage =
|
|
24
|
-
|
|
25
|
-
getNetworkErrorDescription(error)?.replace(/\+/g, " ");
|
|
23
|
+
const errorMessage = getErrorMessage(error);
|
|
24
|
+
const networkErrorMessage = getNetworkErrorMessage(error);
|
|
26
25
|
console.error(error);
|
|
27
26
|
|
|
28
27
|
function onRetry() {
|
|
@@ -33,7 +32,7 @@ export const ErrorPage = (props: ErrorPageProps) => {
|
|
|
33
32
|
<Page>
|
|
34
33
|
<Modal
|
|
35
34
|
variant={ModalVariant.small}
|
|
36
|
-
title={
|
|
35
|
+
title={t("somethingWentWrong")}
|
|
37
36
|
titleIconVariant="danger"
|
|
38
37
|
showClose={false}
|
|
39
38
|
isOpen
|
|
@@ -46,6 +45,8 @@ export const ErrorPage = (props: ErrorPageProps) => {
|
|
|
46
45
|
<TextContent>
|
|
47
46
|
{errorMessage ? (
|
|
48
47
|
<Text>{t(errorMessage)}</Text>
|
|
48
|
+
) : networkErrorMessage && i18n.exists(networkErrorMessage) ? (
|
|
49
|
+
<Text>{t(networkErrorMessage)}</Text>
|
|
49
50
|
) : (
|
|
50
51
|
<Text>{t("somethingWentWrongDescription")}</Text>
|
|
51
52
|
)}
|
|
@@ -56,10 +57,6 @@ export const ErrorPage = (props: ErrorPageProps) => {
|
|
|
56
57
|
};
|
|
57
58
|
|
|
58
59
|
function getErrorMessage(error: unknown): string | null {
|
|
59
|
-
if (typeof error === "string") {
|
|
60
|
-
return error;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
60
|
if (error instanceof Error) {
|
|
64
61
|
return error.message;
|
|
65
62
|
}
|
|
@@ -71,7 +71,7 @@ export const KeycloakProvider = <T extends BaseEnvironment>({
|
|
|
71
71
|
|
|
72
72
|
const init = () =>
|
|
73
73
|
keycloak.init({
|
|
74
|
-
onLoad: "
|
|
74
|
+
onLoad: "login-required",
|
|
75
75
|
pkceMethod: "S256",
|
|
76
76
|
responseMode: "query",
|
|
77
77
|
scope: environment.scope,
|
|
@@ -84,14 +84,8 @@ export const KeycloakProvider = <T extends BaseEnvironment>({
|
|
|
84
84
|
calledOnce.current = true;
|
|
85
85
|
}, [keycloak]);
|
|
86
86
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (error || searchParams.get("error_description")) {
|
|
90
|
-
return (
|
|
91
|
-
<ErrorPage
|
|
92
|
-
error={error ? error : searchParams.get("error_description")}
|
|
93
|
-
/>
|
|
94
|
-
);
|
|
87
|
+
if (error) {
|
|
88
|
+
return <ErrorPage error={error} />;
|
|
95
89
|
}
|
|
96
90
|
|
|
97
91
|
if (!init) {
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
|
|
3
|
+
// @ts-nocheck
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
FileUpload,
|
|
7
|
+
ValidatedOptions,
|
|
8
|
+
FileUploadProps,
|
|
9
|
+
} from "../../@patternfly/react-core";
|
|
10
|
+
import { ReactNode, useState } from "react";
|
|
11
|
+
import {
|
|
12
|
+
FieldPath,
|
|
13
|
+
FieldValues,
|
|
14
|
+
PathValue,
|
|
15
|
+
UseControllerProps,
|
|
16
|
+
useController,
|
|
17
|
+
} from "react-hook-form";
|
|
18
|
+
import { getRuleValue } from "../utils/getRuleValue";
|
|
19
|
+
import { FormLabel } from "./FormLabel";
|
|
20
|
+
import { useTranslation } from "react-i18next";
|
|
21
|
+
|
|
22
|
+
export type FileUploadControlProps<
|
|
23
|
+
T extends FieldValues,
|
|
24
|
+
P extends FieldPath<T> = FieldPath<T>,
|
|
25
|
+
> = UseControllerProps<T, P> &
|
|
26
|
+
Omit<FileUploadProps, "name" | "isRequired" | "required"> & {
|
|
27
|
+
label: string;
|
|
28
|
+
labelIcon?: string | ReactNode;
|
|
29
|
+
isDisabled?: boolean;
|
|
30
|
+
"data-testid"?: string;
|
|
31
|
+
type?: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const FileUploadControl = <
|
|
35
|
+
T extends FieldValues,
|
|
36
|
+
P extends FieldPath<T> = FieldPath<T>,
|
|
37
|
+
>(
|
|
38
|
+
props: FileUploadControlProps<T, P>,
|
|
39
|
+
) => {
|
|
40
|
+
const { labelIcon, ...rest } = props;
|
|
41
|
+
const required = !!getRuleValue(props.rules?.required);
|
|
42
|
+
const defaultValue = props.defaultValue ?? ("" as PathValue<T, P>);
|
|
43
|
+
|
|
44
|
+
const { t } = useTranslation();
|
|
45
|
+
|
|
46
|
+
const [filename, setFilename] = useState<string>("");
|
|
47
|
+
|
|
48
|
+
const { field, fieldState } = useController({
|
|
49
|
+
...props,
|
|
50
|
+
defaultValue,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<FormLabel
|
|
55
|
+
name={props.name}
|
|
56
|
+
label={props.label}
|
|
57
|
+
labelIcon={labelIcon}
|
|
58
|
+
isRequired={required}
|
|
59
|
+
error={fieldState.error}
|
|
60
|
+
>
|
|
61
|
+
<FileUpload
|
|
62
|
+
isRequired={required}
|
|
63
|
+
data-testid={props["data-testid"] || props.name}
|
|
64
|
+
filename={filename}
|
|
65
|
+
browseButtonText={t("browse")}
|
|
66
|
+
validated={
|
|
67
|
+
fieldState.error ? ValidatedOptions.error : ValidatedOptions.default
|
|
68
|
+
}
|
|
69
|
+
hideDefaultPreview
|
|
70
|
+
isDisabled={props.isDisabled}
|
|
71
|
+
type="text"
|
|
72
|
+
onFileInputChange={(_, file) => {
|
|
73
|
+
field.onChange(file);
|
|
74
|
+
setFilename(file.name);
|
|
75
|
+
}}
|
|
76
|
+
onClearClick={() => {
|
|
77
|
+
field.onChange(null);
|
|
78
|
+
setFilename("");
|
|
79
|
+
}}
|
|
80
|
+
{...rest}
|
|
81
|
+
{...field}
|
|
82
|
+
/>
|
|
83
|
+
</FormLabel>
|
|
84
|
+
);
|
|
85
|
+
};
|
|
@@ -9,6 +9,7 @@ import { FormErrorText } from "./FormErrorText";
|
|
|
9
9
|
import { HelpItem } from "./HelpItem";
|
|
10
10
|
|
|
11
11
|
export type FieldProps<T extends FieldValues = FieldValues> = {
|
|
12
|
+
id?: string | undefined;
|
|
12
13
|
label?: string;
|
|
13
14
|
name: string;
|
|
14
15
|
labelIcon?: string | ReactNode;
|
|
@@ -19,6 +20,7 @@ export type FieldProps<T extends FieldValues = FieldValues> = {
|
|
|
19
20
|
type FormLabelProps = FieldProps & Omit<FormGroupProps, "label" | "labelIcon">;
|
|
20
21
|
|
|
21
22
|
export const FormLabel = ({
|
|
23
|
+
id,
|
|
22
24
|
name,
|
|
23
25
|
label,
|
|
24
26
|
labelIcon,
|
|
@@ -28,10 +30,10 @@ export const FormLabel = ({
|
|
|
28
30
|
}: PropsWithChildren<FormLabelProps>) => (
|
|
29
31
|
<FormGroup
|
|
30
32
|
label={label || name}
|
|
31
|
-
fieldId={name}
|
|
33
|
+
fieldId={id || name}
|
|
32
34
|
labelIcon={
|
|
33
35
|
labelIcon ? (
|
|
34
|
-
<HelpItem helpText={labelIcon} fieldLabelId={name} />
|
|
36
|
+
<HelpItem helpText={labelIcon} fieldLabelId={id || name} />
|
|
35
37
|
) : undefined
|
|
36
38
|
}
|
|
37
39
|
{...rest}
|
|
@@ -67,6 +67,7 @@ export type OrganizationTableProps = PropsWithChildren & {
|
|
|
67
67
|
toolbarItem?: ReactNode;
|
|
68
68
|
isPaginated?: boolean;
|
|
69
69
|
isSearching?: boolean;
|
|
70
|
+
searchPlaceholderKey?: string;
|
|
70
71
|
onSelect?: (orgs: OrganizationRepresentation[]) => void;
|
|
71
72
|
onDelete?: (org: OrganizationRepresentation) => void;
|
|
72
73
|
deleteLabel?: string;
|
|
@@ -77,6 +78,7 @@ export const OrganizationTable = ({
|
|
|
77
78
|
toolbarItem,
|
|
78
79
|
isPaginated = false,
|
|
79
80
|
isSearching = false,
|
|
81
|
+
searchPlaceholderKey,
|
|
80
82
|
onSelect,
|
|
81
83
|
onDelete,
|
|
82
84
|
deleteLabel = "delete",
|
|
@@ -91,6 +93,7 @@ export const OrganizationTable = ({
|
|
|
91
93
|
isPaginated={isPaginated}
|
|
92
94
|
isSearching={isSearching}
|
|
93
95
|
ariaLabelKey="organizationList"
|
|
96
|
+
searchPlaceholderKey={searchPlaceholderKey}
|
|
94
97
|
toolbarItem={toolbarItem}
|
|
95
98
|
onSelect={onSelect}
|
|
96
99
|
canSelectAll={onSelect !== undefined}
|
|
@@ -12,12 +12,13 @@ import {
|
|
|
12
12
|
} from "react-hook-form";
|
|
13
13
|
import { SwitchProps, Switch } from "../../@patternfly/react-core";
|
|
14
14
|
import { FormLabel } from "./FormLabel";
|
|
15
|
+
import { debeerify } from "../user-profile/utils";
|
|
15
16
|
|
|
16
17
|
export type SwitchControlProps<
|
|
17
18
|
T extends FieldValues,
|
|
18
19
|
P extends FieldPath<T> = FieldPath<T>,
|
|
19
20
|
> = Omit<SwitchProps, "name" | "defaultValue" | "ref"> &
|
|
20
|
-
UseControllerProps<
|
|
21
|
+
UseControllerProps<any, P> & {
|
|
21
22
|
name: string;
|
|
22
23
|
label?: string;
|
|
23
24
|
labelIcon?: string;
|
|
@@ -55,7 +56,7 @@ export const SwitchControl = <
|
|
|
55
56
|
<Switch
|
|
56
57
|
{...props}
|
|
57
58
|
id={props.name}
|
|
58
|
-
data-testid={props.name}
|
|
59
|
+
data-testid={debeerify(props.name)}
|
|
59
60
|
label={labelOn}
|
|
60
61
|
isChecked={stringify ? value === "true" : value}
|
|
61
62
|
onChange={(e, checked) => {
|
|
@@ -45,6 +45,7 @@ export type SelectControlProps<
|
|
|
45
45
|
name: string;
|
|
46
46
|
label?: string;
|
|
47
47
|
options: OptionType;
|
|
48
|
+
selectedOptions?: OptionType;
|
|
48
49
|
labelIcon?: string;
|
|
49
50
|
controller: Omit<ControllerProps, "name" | "render">;
|
|
50
51
|
onFilter?: (value: string) => void;
|
|
@@ -53,6 +54,10 @@ export type SelectControlProps<
|
|
|
53
54
|
menuAppendTo?: string;
|
|
54
55
|
placeholderText?: string;
|
|
55
56
|
chipGroupProps?: ChipGroupProps;
|
|
57
|
+
onSelect?: (
|
|
58
|
+
value: string | string[],
|
|
59
|
+
onChangeHandler: (value: string | string[]) => void,
|
|
60
|
+
) => void;
|
|
56
61
|
};
|
|
57
62
|
|
|
58
63
|
export const isSelectBasedOptions = (
|
package/keycloak-theme/shared/keycloak-ui-shared/controls/select-control/SingleSelectControl.tsx
CHANGED
|
@@ -34,8 +34,11 @@ export const SingleSelectControl = <
|
|
|
34
34
|
name,
|
|
35
35
|
label,
|
|
36
36
|
options,
|
|
37
|
+
selectedOptions = [],
|
|
37
38
|
controller,
|
|
38
39
|
labelIcon,
|
|
40
|
+
isDisabled,
|
|
41
|
+
onSelect,
|
|
39
42
|
...rest
|
|
40
43
|
}: SelectControlProps<T, P>) => {
|
|
41
44
|
const {
|
|
@@ -47,6 +50,7 @@ export const SingleSelectControl = <
|
|
|
47
50
|
|
|
48
51
|
return (
|
|
49
52
|
<FormLabel
|
|
53
|
+
id={id}
|
|
50
54
|
name={name}
|
|
51
55
|
label={label}
|
|
52
56
|
isRequired={required}
|
|
@@ -60,6 +64,7 @@ export const SingleSelectControl = <
|
|
|
60
64
|
render={({ field: { onChange, value } }) => (
|
|
61
65
|
<Select
|
|
62
66
|
{...rest}
|
|
67
|
+
variant="default"
|
|
63
68
|
onClick={() => setOpen(!open)}
|
|
64
69
|
onOpenChange={() => setOpen(false)}
|
|
65
70
|
selected={
|
|
@@ -75,13 +80,14 @@ export const SingleSelectControl = <
|
|
|
75
80
|
}
|
|
76
81
|
toggle={(ref) => (
|
|
77
82
|
<MenuToggle
|
|
78
|
-
id={id || name
|
|
83
|
+
id={id || name}
|
|
79
84
|
ref={ref}
|
|
80
85
|
onClick={() => setOpen(!open)}
|
|
81
86
|
isExpanded={open}
|
|
82
87
|
isFullWidth
|
|
83
88
|
status={get(errors, name) ? MenuToggleStatus.danger : undefined}
|
|
84
|
-
aria-label=
|
|
89
|
+
aria-label={label}
|
|
90
|
+
isDisabled={isDisabled}
|
|
85
91
|
>
|
|
86
92
|
{isSelectBasedOptions(options)
|
|
87
93
|
? options.find(
|
|
@@ -92,14 +98,19 @@ export const SingleSelectControl = <
|
|
|
92
98
|
</MenuToggle>
|
|
93
99
|
)}
|
|
94
100
|
onSelect={(_event, v) => {
|
|
95
|
-
const option = v?.toString()
|
|
96
|
-
|
|
101
|
+
const option = v?.toString()!;
|
|
102
|
+
const convertedValue = Array.isArray(value) ? [option] : option;
|
|
103
|
+
if (onSelect) {
|
|
104
|
+
onSelect(convertedValue, onChange);
|
|
105
|
+
} else {
|
|
106
|
+
onChange(convertedValue);
|
|
107
|
+
}
|
|
97
108
|
setOpen(false);
|
|
98
109
|
}}
|
|
99
110
|
isOpen={open}
|
|
100
111
|
>
|
|
101
112
|
<SelectList data-testid={`select-${name}`}>
|
|
102
|
-
{options.map((option) => (
|
|
113
|
+
{[...options, ...selectedOptions].map((option) => (
|
|
103
114
|
<SelectOption key={key(option)} value={key(option)}>
|
|
104
115
|
{isString(option) ? option : option.value}
|
|
105
116
|
</SelectOption>
|
package/keycloak-theme/shared/keycloak-ui-shared/controls/select-control/TypeaheadSelectControl.tsx
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
import { getRuleValue } from "../../utils/getRuleValue";
|
|
29
29
|
import { FormLabel } from "../FormLabel";
|
|
30
30
|
import {
|
|
31
|
+
OptionType,
|
|
31
32
|
SelectControlOption,
|
|
32
33
|
SelectControlProps,
|
|
33
34
|
SelectVariant,
|
|
@@ -47,6 +48,7 @@ export const TypeaheadSelectControl = <
|
|
|
47
48
|
name,
|
|
48
49
|
label,
|
|
49
50
|
options,
|
|
51
|
+
selectedOptions = [],
|
|
50
52
|
controller,
|
|
51
53
|
labelIcon,
|
|
52
54
|
placeholderText,
|
|
@@ -61,28 +63,50 @@ export const TypeaheadSelectControl = <
|
|
|
61
63
|
const [open, setOpen] = useState(false);
|
|
62
64
|
const [filterValue, setFilterValue] = useState("");
|
|
63
65
|
const [focusedItemIndex, setFocusedItemIndex] = useState<number>(0);
|
|
66
|
+
const [selectedOptionsState, setSelectedOptions] = useState<
|
|
67
|
+
SelectControlOption[]
|
|
68
|
+
>([]);
|
|
64
69
|
const textInputRef = useRef<HTMLInputElement>();
|
|
65
70
|
const required = getRuleValue(controller.rules?.required) === true;
|
|
66
71
|
const isTypeaheadMulti = variant === SelectVariant.typeaheadMulti;
|
|
67
72
|
|
|
68
|
-
const
|
|
69
|
-
|
|
73
|
+
const combinedOptions = useMemo(
|
|
74
|
+
() =>
|
|
75
|
+
[
|
|
76
|
+
...options.filter(
|
|
77
|
+
(o) => !selectedOptions.map((o) => getValue(o)).includes(getValue(o)),
|
|
78
|
+
),
|
|
79
|
+
...selectedOptions,
|
|
80
|
+
] as OptionType,
|
|
81
|
+
[selectedOptions, options],
|
|
70
82
|
);
|
|
71
83
|
|
|
72
|
-
const
|
|
73
|
-
()
|
|
74
|
-
filteredOptions.map((option, index) => (
|
|
75
|
-
<SelectOption
|
|
76
|
-
key={key(option)}
|
|
77
|
-
value={key(option)}
|
|
78
|
-
isFocused={focusedItemIndex === index}
|
|
79
|
-
>
|
|
80
|
-
{getValue(option)}
|
|
81
|
-
</SelectOption>
|
|
82
|
-
)),
|
|
83
|
-
[focusedItemIndex, filteredOptions],
|
|
84
|
+
const filteredOptions = combinedOptions.filter((option) =>
|
|
85
|
+
getValue(option).toLowerCase().startsWith(filterValue.toLowerCase()),
|
|
84
86
|
);
|
|
85
87
|
|
|
88
|
+
const updateValue = (
|
|
89
|
+
option: string | string[],
|
|
90
|
+
field: ControllerRenderProps<FieldValues, string>,
|
|
91
|
+
) => {
|
|
92
|
+
if (field.value.includes(option)) {
|
|
93
|
+
field.onChange(field.value.filter((item: string) => item !== option));
|
|
94
|
+
if (isSelectBasedOptions(options)) {
|
|
95
|
+
setSelectedOptions(
|
|
96
|
+
selectedOptionsState.filter((item) => item.key !== option),
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
field.onChange([...field.value, option]);
|
|
101
|
+
if (isSelectBasedOptions(combinedOptions)) {
|
|
102
|
+
setSelectedOptions([
|
|
103
|
+
...selectedOptionsState,
|
|
104
|
+
combinedOptions.find((o) => o.key === option)!,
|
|
105
|
+
]);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
86
110
|
const onInputKeyDown = (
|
|
87
111
|
event: React.KeyboardEvent<HTMLDivElement>,
|
|
88
112
|
field: ControllerRenderProps<FieldValues, string>,
|
|
@@ -100,11 +124,8 @@ export const TypeaheadSelectControl = <
|
|
|
100
124
|
setFilterValue("");
|
|
101
125
|
}
|
|
102
126
|
|
|
103
|
-
field
|
|
104
|
-
|
|
105
|
-
? [...field.value, key(focusedItem)]
|
|
106
|
-
: key(focusedItem),
|
|
107
|
-
);
|
|
127
|
+
updateValue(key(focusedItem), field);
|
|
128
|
+
|
|
108
129
|
setOpen(false);
|
|
109
130
|
setFocusedItemIndex(0);
|
|
110
131
|
|
|
@@ -152,6 +173,7 @@ export const TypeaheadSelectControl = <
|
|
|
152
173
|
|
|
153
174
|
return (
|
|
154
175
|
<FormLabel
|
|
176
|
+
id={id}
|
|
155
177
|
name={name}
|
|
156
178
|
label={label}
|
|
157
179
|
isRequired={required}
|
|
@@ -167,8 +189,8 @@ export const TypeaheadSelectControl = <
|
|
|
167
189
|
{...rest}
|
|
168
190
|
onOpenChange={() => setOpen(false)}
|
|
169
191
|
selected={
|
|
170
|
-
isSelectBasedOptions(
|
|
171
|
-
?
|
|
192
|
+
isSelectBasedOptions(combinedOptions)
|
|
193
|
+
? combinedOptions
|
|
172
194
|
.filter((o) =>
|
|
173
195
|
Array.isArray(field.value)
|
|
174
196
|
? field.value.includes(o.key)
|
|
@@ -181,7 +203,7 @@ export const TypeaheadSelectControl = <
|
|
|
181
203
|
toggle={(ref) => (
|
|
182
204
|
<MenuToggle
|
|
183
205
|
ref={ref}
|
|
184
|
-
id={id || name
|
|
206
|
+
id={id || name}
|
|
185
207
|
variant="typeahead"
|
|
186
208
|
onClick={() => {
|
|
187
209
|
setOpen(!open);
|
|
@@ -196,8 +218,8 @@ export const TypeaheadSelectControl = <
|
|
|
196
218
|
placeholder={placeholderText}
|
|
197
219
|
value={
|
|
198
220
|
variant === SelectVariant.typeahead && field.value
|
|
199
|
-
? isSelectBasedOptions(
|
|
200
|
-
?
|
|
221
|
+
? isSelectBasedOptions(combinedOptions)
|
|
222
|
+
? combinedOptions.find(
|
|
201
223
|
(o) =>
|
|
202
224
|
o.key ===
|
|
203
225
|
(Array.isArray(field.value)
|
|
@@ -235,9 +257,11 @@ export const TypeaheadSelectControl = <
|
|
|
235
257
|
);
|
|
236
258
|
}}
|
|
237
259
|
>
|
|
238
|
-
{isSelectBasedOptions(
|
|
239
|
-
?
|
|
240
|
-
|
|
260
|
+
{isSelectBasedOptions(combinedOptions)
|
|
261
|
+
? [
|
|
262
|
+
...combinedOptions,
|
|
263
|
+
...selectedOptionsState,
|
|
264
|
+
].find((o) => selection === o.key)?.value
|
|
241
265
|
: getValue(selection)}
|
|
242
266
|
</Chip>
|
|
243
267
|
),
|
|
@@ -267,13 +291,8 @@ export const TypeaheadSelectControl = <
|
|
|
267
291
|
event?.stopPropagation();
|
|
268
292
|
const option = v?.toString();
|
|
269
293
|
if (isTypeaheadMulti && Array.isArray(field.value)) {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
field.value.filter((item: string) => item !== option),
|
|
273
|
-
);
|
|
274
|
-
} else {
|
|
275
|
-
field.onChange([...field.value, option]);
|
|
276
|
-
}
|
|
294
|
+
setFilterValue("");
|
|
295
|
+
updateValue(option || "", field);
|
|
277
296
|
} else {
|
|
278
297
|
field.onChange(Array.isArray(field.value) ? [option] : option);
|
|
279
298
|
setOpen(false);
|
|
@@ -281,7 +300,18 @@ export const TypeaheadSelectControl = <
|
|
|
281
300
|
}}
|
|
282
301
|
isOpen={open}
|
|
283
302
|
>
|
|
284
|
-
<SelectList>
|
|
303
|
+
<SelectList>
|
|
304
|
+
{filteredOptions.map((option, index) => (
|
|
305
|
+
<SelectOption
|
|
306
|
+
key={key(option)}
|
|
307
|
+
value={key(option)}
|
|
308
|
+
isFocused={focusedItemIndex === index}
|
|
309
|
+
isActive={field.value.includes(getValue(option))}
|
|
310
|
+
>
|
|
311
|
+
{getValue(option)}
|
|
312
|
+
</SelectOption>
|
|
313
|
+
))}
|
|
314
|
+
</SelectList>
|
|
285
315
|
</Select>
|
|
286
316
|
)}
|
|
287
317
|
/>
|
|
@@ -92,21 +92,21 @@ const CellRenderer = ({
|
|
|
92
92
|
index,
|
|
93
93
|
actions,
|
|
94
94
|
actionResolver,
|
|
95
|
-
}: CellRendererProps) =>
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
extraData={{ rowIndex: index }}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
95
|
+
}: CellRendererProps) => {
|
|
96
|
+
const items = actions || actionResolver?.(row, {});
|
|
97
|
+
return (
|
|
98
|
+
<>
|
|
99
|
+
{row.cells!.map((c, i) => (
|
|
100
|
+
<Td key={`cell-${i}`}>{(isRow(c) ? c.title : c) as ReactNode}</Td>
|
|
101
|
+
))}
|
|
102
|
+
{items && items.length > 0 && !row.disableActions && (
|
|
103
|
+
<Td isActionCell>
|
|
104
|
+
<ActionsColumn items={items} extraData={{ rowIndex: index }} />
|
|
105
|
+
</Td>
|
|
106
|
+
)}
|
|
107
|
+
</>
|
|
108
|
+
);
|
|
109
|
+
};
|
|
110
110
|
|
|
111
111
|
const ExpandableRowRenderer = ({ row }: CellRendererProps) =>
|
|
112
112
|
row.cells!.map((c, i) => (
|
|
@@ -160,24 +160,31 @@ function DataTable<T>({
|
|
|
160
160
|
};
|
|
161
161
|
|
|
162
162
|
const updateState = (rowIndex: number, isSelected: boolean) => {
|
|
163
|
-
if (
|
|
164
|
-
const
|
|
165
|
-
updateSelectedRows(
|
|
166
|
-
isSelected
|
|
167
|
-
? [...selectedRows, ...rows.map((row) => row.data)]
|
|
168
|
-
: selectedRows.filter(
|
|
169
|
-
(v) => !rowsSelectedOnPageIds.includes(get(v, "id")),
|
|
170
|
-
),
|
|
171
|
-
);
|
|
163
|
+
if (isRadio) {
|
|
164
|
+
const selectedRow = isSelected ? [rows[rowIndex].data] : [];
|
|
165
|
+
updateSelectedRows(selectedRow);
|
|
172
166
|
} else {
|
|
173
|
-
if (
|
|
174
|
-
|
|
175
|
-
|
|
167
|
+
if (rowIndex === -1) {
|
|
168
|
+
const rowsSelectedOnPageIds = rowsSelectedOnPage.map((v) =>
|
|
169
|
+
get(v, "id"),
|
|
170
|
+
);
|
|
176
171
|
updateSelectedRows(
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
172
|
+
isSelected
|
|
173
|
+
? [...selectedRows, ...rows.map((row) => row.data)]
|
|
174
|
+
: selectedRows.filter(
|
|
175
|
+
(v) => !rowsSelectedOnPageIds.includes(get(v, "id")),
|
|
176
|
+
),
|
|
180
177
|
);
|
|
178
|
+
} else {
|
|
179
|
+
if (isSelected) {
|
|
180
|
+
updateSelectedRows([...selectedRows, rows[rowIndex].data]);
|
|
181
|
+
} else {
|
|
182
|
+
updateSelectedRows(
|
|
183
|
+
selectedRows.filter(
|
|
184
|
+
(v) => get(v, "id") !== (rows[rowIndex] as IRow).data.id,
|
|
185
|
+
),
|
|
186
|
+
);
|
|
187
|
+
}
|
|
181
188
|
}
|
|
182
189
|
}
|
|
183
190
|
};
|
|
@@ -232,6 +239,7 @@ function DataTable<T>({
|
|
|
232
239
|
(v) => get(v, "id") === row.data.id,
|
|
233
240
|
),
|
|
234
241
|
variant: isRadio ? "radio" : "checkbox",
|
|
242
|
+
isDisabled: row.disableSelection,
|
|
235
243
|
}}
|
|
236
244
|
/>
|
|
237
245
|
)}
|
|
@@ -250,17 +258,21 @@ function DataTable<T>({
|
|
|
250
258
|
{index % 2 === 0 ? (
|
|
251
259
|
<Tr>
|
|
252
260
|
<Td
|
|
253
|
-
expand={
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
261
|
+
expand={
|
|
262
|
+
rows[index + 1].cells.length === 0
|
|
263
|
+
? undefined
|
|
264
|
+
: {
|
|
265
|
+
isExpanded: !!expandedRows[index],
|
|
266
|
+
rowIndex: index,
|
|
267
|
+
expandId: "expandable-row-",
|
|
268
|
+
onToggle: (_, rowIndex, isOpen) => {
|
|
269
|
+
onCollapse(isOpen, rowIndex);
|
|
270
|
+
const expand = [...expandedRows];
|
|
271
|
+
expand[index] = isOpen;
|
|
272
|
+
setExpandedRows(expand);
|
|
273
|
+
},
|
|
274
|
+
}
|
|
275
|
+
}
|
|
264
276
|
/>
|
|
265
277
|
<CellRenderer
|
|
266
278
|
row={row}
|
|
@@ -552,7 +564,7 @@ export function KeycloakDataTable<T>({
|
|
|
552
564
|
|
|
553
565
|
return (
|
|
554
566
|
<>
|
|
555
|
-
{(
|
|
567
|
+
{(!noData || searching) && (
|
|
556
568
|
<PaginatingTableToolbar
|
|
557
569
|
id={id}
|
|
558
570
|
count={rowLength}
|
|
@@ -575,7 +587,7 @@ export function KeycloakDataTable<T>({
|
|
|
575
587
|
<>
|
|
576
588
|
{toolbarItem} <ToolbarItem variant="separator" />{" "}
|
|
577
589
|
<ToolbarItem>
|
|
578
|
-
<Button variant="link" onClick={refresh}>
|
|
590
|
+
<Button variant="link" onClick={refresh} data-testid="refresh">
|
|
579
591
|
<SyncAltIcon /> {t("refresh")}
|
|
580
592
|
</Button>
|
|
581
593
|
</ToolbarItem>
|
|
@@ -623,9 +635,9 @@ export function KeycloakDataTable<T>({
|
|
|
623
635
|
}
|
|
624
636
|
/>
|
|
625
637
|
)}
|
|
626
|
-
{loading && <KeycloakSpinner />}
|
|
627
638
|
</PaginatingTableToolbar>
|
|
628
639
|
)}
|
|
640
|
+
{loading && <KeycloakSpinner />}
|
|
629
641
|
{!loading && noData && !searching && emptyState}
|
|
630
642
|
</>
|
|
631
643
|
);
|
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
// @ts-nocheck
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
ComponentClass,
|
|
7
|
+
MouseEventHandler,
|
|
8
|
+
PropsWithChildren,
|
|
9
|
+
ReactNode,
|
|
10
|
+
} from "react";
|
|
6
11
|
import {
|
|
7
12
|
EmptyState,
|
|
8
13
|
EmptyStateIcon,
|
|
@@ -44,7 +49,8 @@ export const ListEmptyState = ({
|
|
|
44
49
|
secondaryActions,
|
|
45
50
|
icon,
|
|
46
51
|
isDisabled = false,
|
|
47
|
-
|
|
52
|
+
children,
|
|
53
|
+
}: PropsWithChildren<ListEmptyStateProps>) => {
|
|
48
54
|
return (
|
|
49
55
|
<EmptyState data-testid="empty-state" variant="lg">
|
|
50
56
|
{hasIcon && isSearchVariant ? (
|
|
@@ -67,6 +73,7 @@ export const ListEmptyState = ({
|
|
|
67
73
|
{primaryActionText}
|
|
68
74
|
</Button>
|
|
69
75
|
)}
|
|
76
|
+
{children}
|
|
70
77
|
{secondaryActions && (
|
|
71
78
|
<EmptyStateActions>
|
|
72
79
|
{secondaryActions.map((action) => (
|
|
@@ -47,6 +47,10 @@ export {
|
|
|
47
47
|
KeycloakTextArea,
|
|
48
48
|
type KeycloakTextAreaProps,
|
|
49
49
|
} from "./controls/keycloak-text-area/KeycloakTextArea";
|
|
50
|
+
export {
|
|
51
|
+
FileUploadControl,
|
|
52
|
+
type FileUploadControlProps,
|
|
53
|
+
} from "./controls/FileUploadControl";
|
|
50
54
|
export { IconMapper } from "./icons/IconMapper";
|
|
51
55
|
export { FormPanel } from "./scroll-form/FormPanel";
|
|
52
56
|
export { ScrollForm, mainPageContentId } from "./scroll-form/ScrollForm";
|
|
@@ -58,6 +58,7 @@ type KeycloakMastheadProps = MastheadMainProps & {
|
|
|
58
58
|
kebabDropdownItems?: ReactNode[];
|
|
59
59
|
dropdownItems?: ReactNode[];
|
|
60
60
|
toolbarItems?: ReactNode[];
|
|
61
|
+
toolbar?: ReactNode;
|
|
61
62
|
};
|
|
62
63
|
|
|
63
64
|
const KeycloakMasthead = ({
|
|
@@ -72,6 +73,7 @@ const KeycloakMasthead = ({
|
|
|
72
73
|
kebabDropdownItems,
|
|
73
74
|
dropdownItems = [],
|
|
74
75
|
toolbarItems,
|
|
76
|
+
toolbar,
|
|
75
77
|
...rest
|
|
76
78
|
}: KeycloakMastheadProps) => {
|
|
77
79
|
const { t } = useTranslation();
|
|
@@ -106,6 +108,7 @@ const KeycloakMasthead = ({
|
|
|
106
108
|
<img src={src} alt={alt} className={className} />
|
|
107
109
|
</MastheadBrand>
|
|
108
110
|
<MastheadContent>
|
|
111
|
+
{toolbar}
|
|
109
112
|
<Toolbar>
|
|
110
113
|
<ToolbarContent>
|
|
111
114
|
{toolbarItems?.map((item, index) => (
|
|
@@ -84,10 +84,17 @@ export const ScrollForm = ({
|
|
|
84
84
|
const scrollId = spacesToHyphens(title.toLowerCase());
|
|
85
85
|
|
|
86
86
|
return (
|
|
87
|
-
// note that JumpLinks currently does not work with spaces in the href
|
|
88
87
|
<JumpLinksItem
|
|
89
88
|
key={title}
|
|
90
|
-
|
|
89
|
+
onClick={() => {
|
|
90
|
+
const element = document.getElementById(scrollId);
|
|
91
|
+
if (element) {
|
|
92
|
+
element.scrollIntoView({
|
|
93
|
+
behavior: "smooth",
|
|
94
|
+
block: "start",
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}}
|
|
91
98
|
data-testid={`jump-link-${scrollId}`}
|
|
92
99
|
>
|
|
93
100
|
{title}
|
|
@@ -19,7 +19,7 @@ export const propertyToString = (prop: string | number | undefined) =>
|
|
|
19
19
|
|
|
20
20
|
export type KeycloakSelectProps = Omit<
|
|
21
21
|
SelectProps,
|
|
22
|
-
"name" | "toggle" | "selected" | "onClick" | "onSelect"
|
|
22
|
+
"name" | "toggle" | "selected" | "onClick" | "onSelect" | "variant"
|
|
23
23
|
> & {
|
|
24
24
|
toggleId?: string;
|
|
25
25
|
onFilter?: (value: string) => JSX.Element[];
|
|
@@ -119,7 +119,11 @@ export const TypeaheadSelect = ({
|
|
|
119
119
|
{...rest}
|
|
120
120
|
onClick={toggle}
|
|
121
121
|
onOpenChange={(isOpen) => onToggle?.(isOpen)}
|
|
122
|
-
onSelect={(_, value) =>
|
|
122
|
+
onSelect={(_, value) => {
|
|
123
|
+
onSelect?.(value || "");
|
|
124
|
+
onFilter?.("");
|
|
125
|
+
setFilterValue("");
|
|
126
|
+
}}
|
|
123
127
|
maxMenuHeight={propertyToString(maxHeight)}
|
|
124
128
|
popperProps={{ direction, width: propertyToString(width) }}
|
|
125
129
|
toggle={(ref) => (
|
|
@@ -47,7 +47,7 @@ export const LocaleSelector = ({
|
|
|
47
47
|
name="attributes.locale"
|
|
48
48
|
label={t("selectALocale")}
|
|
49
49
|
controller={{ defaultValue: "" }}
|
|
50
|
-
options={locales}
|
|
50
|
+
options={[{ key: "", value: t("defaultLocale") }, ...locales]}
|
|
51
51
|
variant={locales.length >= 10 ? "typeahead" : "single"}
|
|
52
52
|
/>
|
|
53
53
|
</FormProvider>
|
|
@@ -27,13 +27,13 @@ export const TextComponent = (props: UserProfileFieldProps) => {
|
|
|
27
27
|
: label(
|
|
28
28
|
props.t,
|
|
29
29
|
attribute.annotations?.["inputTypePlaceholder"] as string,
|
|
30
|
-
|
|
30
|
+
"",
|
|
31
31
|
attribute.annotations?.[
|
|
32
32
|
"inputOptionLabelsI18nPrefix"
|
|
33
33
|
] as string,
|
|
34
34
|
)
|
|
35
35
|
}
|
|
36
|
-
|
|
36
|
+
isDisabled={attribute.readOnly}
|
|
37
37
|
isRequired={isRequired}
|
|
38
38
|
{...form.register(fieldName(attribute.name))}
|
|
39
39
|
/>
|
|
@@ -19,7 +19,7 @@ export type UserFormFields = Omit<
|
|
|
19
19
|
type FieldError = {
|
|
20
20
|
field: string;
|
|
21
21
|
errorMessage: string;
|
|
22
|
-
params?:
|
|
22
|
+
params?: unknown[];
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
type ErrorArray = { errors?: FieldError[] };
|
|
@@ -28,7 +28,11 @@ export type UserProfileError = {
|
|
|
28
28
|
responseData: ErrorArray | FieldError;
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
-
const isBundleKey = (displayName
|
|
31
|
+
const isBundleKey = (displayName: unknown) => {
|
|
32
|
+
return displayName && typeof displayName === "string"
|
|
33
|
+
? displayName.includes("${")
|
|
34
|
+
: false;
|
|
35
|
+
};
|
|
32
36
|
const unWrap = (key: string) => key.substring(2, key.length - 1);
|
|
33
37
|
|
|
34
38
|
export const label = (
|
|
@@ -77,12 +81,15 @@ export function setUserProfileServerError<T>(
|
|
|
77
81
|
).forEach((e) => {
|
|
78
82
|
const params = Object.assign(
|
|
79
83
|
{},
|
|
80
|
-
e.params?.map((p) =>
|
|
84
|
+
e.params?.map((p) =>
|
|
85
|
+
isBundleKey(p?.toString()) ? t(unWrap(p as string)) : p,
|
|
86
|
+
),
|
|
81
87
|
);
|
|
82
88
|
setError(fieldName(e.field) as keyof T, {
|
|
83
89
|
message: t(
|
|
84
90
|
isBundleKey(e.errorMessage) ? unWrap(e.errorMessage) : e.errorMessage,
|
|
85
91
|
{
|
|
92
|
+
/* eslint-disable @typescript-eslint/no-misused-spread */
|
|
86
93
|
...params,
|
|
87
94
|
defaultValue: e.errorMessage || e.field,
|
|
88
95
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@keycloakify/keycloak-ui-shared",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "260305.0.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git://github.com/keycloakify/keycloak-ui-shared.git"
|
|
@@ -9,19 +9,19 @@
|
|
|
9
9
|
"author": "The Keycloak Team, re-packaged by u/garronej",
|
|
10
10
|
"homepage": "https://github.com/keycloakify/keycloak-ui-shared",
|
|
11
11
|
"peerDependencies": {
|
|
12
|
-
"@patternfly/react-core": "^5.4.
|
|
12
|
+
"@patternfly/react-core": "^5.4.14",
|
|
13
13
|
"@patternfly/react-icons": "^5.4.2",
|
|
14
14
|
"@patternfly/react-styles": "^5.4.1",
|
|
15
|
-
"@patternfly/react-table": "^5.4.
|
|
16
|
-
"i18next": "^
|
|
15
|
+
"@patternfly/react-table": "^5.4.16",
|
|
16
|
+
"i18next": "^25.2.1",
|
|
17
|
+
"keycloak-js": "^26.2.0",
|
|
17
18
|
"lodash-es": "^4.17.21",
|
|
18
19
|
"react": "^18.3.1",
|
|
19
|
-
"react-hook-form": "7.
|
|
20
|
-
"react-i18next": "^15.
|
|
21
|
-
"@keycloak/keycloak-admin-client": "26.
|
|
22
|
-
"keycloak-js": "26.1.3",
|
|
20
|
+
"react-hook-form": "7.59.0",
|
|
21
|
+
"react-i18next": "^15.5.3",
|
|
22
|
+
"@keycloak/keycloak-admin-client": "26.3.5",
|
|
23
23
|
"@types/lodash-es": "^4.17.12",
|
|
24
|
-
"@types/react": "^18.3.
|
|
24
|
+
"@types/react": "^18.3.18"
|
|
25
25
|
},
|
|
26
26
|
"publishConfig": {
|
|
27
27
|
"access": "public"
|