@keycloakify/keycloak-ui-shared 260200.0.0 → 260500.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/context/environment.ts +1 -1
- 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/SwitchControl.tsx +2 -1
- package/keycloak-theme/shared/keycloak-ui-shared/controls/select-control/SelectControl.tsx +1 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/select-control/SingleSelectControl.tsx +4 -2
- 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 +19 -18
- package/keycloak-theme/shared/keycloak-ui-shared/controls/table/ListEmptyState.tsx +9 -2
- package/keycloak-theme/shared/keycloak-ui-shared/main.ts +4 -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/user-profile/MultiInputComponent.tsx +1 -0
- package/keycloak-theme/shared/keycloak-ui-shared/user-profile/OptionsComponent.tsx +5 -5
- package/keycloak-theme/shared/keycloak-ui-shared/user-profile/SelectComponent.tsx +1 -1
- package/keycloak-theme/shared/keycloak-ui-shared/user-profile/TextAreaComponent.tsx +1 -0
- package/keycloak-theme/shared/keycloak-ui-shared/user-profile/TextComponent.tsx +2 -1
- package/keycloak-theme/shared/keycloak-ui-shared/user-profile/utils.ts +7 -3
- package/package.json +7 -7
|
@@ -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) {
|
|
@@ -19,7 +19,7 @@ export type BaseEnvironment = {
|
|
|
19
19
|
clientId: string;
|
|
20
20
|
/** The base URL of the resources. */
|
|
21
21
|
resourceUrl: string;
|
|
22
|
-
/** The source URL for the
|
|
22
|
+
/** The source URL for the logo image. */
|
|
23
23
|
logo: string;
|
|
24
24
|
/** The URL to be followed when the logo is clicked. */
|
|
25
25
|
logoUrl: string;
|
|
@@ -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}
|
|
@@ -18,7 +18,7 @@ export type SwitchControlProps<
|
|
|
18
18
|
T extends FieldValues,
|
|
19
19
|
P extends FieldPath<T> = FieldPath<T>,
|
|
20
20
|
> = Omit<SwitchProps, "name" | "defaultValue" | "ref"> &
|
|
21
|
-
UseControllerProps<
|
|
21
|
+
UseControllerProps<any, P> & {
|
|
22
22
|
name: string;
|
|
23
23
|
label?: string;
|
|
24
24
|
labelIcon?: string;
|
|
@@ -58,6 +58,7 @@ export const SwitchControl = <
|
|
|
58
58
|
id={props.name}
|
|
59
59
|
data-testid={debeerify(props.name)}
|
|
60
60
|
label={labelOn}
|
|
61
|
+
aria-label={props.label}
|
|
61
62
|
isChecked={stringify ? value === "true" : value}
|
|
62
63
|
onChange={(e, checked) => {
|
|
63
64
|
const value = stringify ? checked.toString() : checked;
|
package/keycloak-theme/shared/keycloak-ui-shared/controls/select-control/SingleSelectControl.tsx
CHANGED
|
@@ -34,6 +34,7 @@ export const SingleSelectControl = <
|
|
|
34
34
|
name,
|
|
35
35
|
label,
|
|
36
36
|
options,
|
|
37
|
+
selectedOptions = [],
|
|
37
38
|
controller,
|
|
38
39
|
labelIcon,
|
|
39
40
|
isDisabled,
|
|
@@ -49,6 +50,7 @@ export const SingleSelectControl = <
|
|
|
49
50
|
|
|
50
51
|
return (
|
|
51
52
|
<FormLabel
|
|
53
|
+
id={id}
|
|
52
54
|
name={name}
|
|
53
55
|
label={label}
|
|
54
56
|
isRequired={required}
|
|
@@ -78,7 +80,7 @@ export const SingleSelectControl = <
|
|
|
78
80
|
}
|
|
79
81
|
toggle={(ref) => (
|
|
80
82
|
<MenuToggle
|
|
81
|
-
id={id || name
|
|
83
|
+
id={id || name}
|
|
82
84
|
ref={ref}
|
|
83
85
|
onClick={() => setOpen(!open)}
|
|
84
86
|
isExpanded={open}
|
|
@@ -108,7 +110,7 @@ export const SingleSelectControl = <
|
|
|
108
110
|
isOpen={open}
|
|
109
111
|
>
|
|
110
112
|
<SelectList data-testid={`select-${name}`}>
|
|
111
|
-
{options.map((option) => (
|
|
113
|
+
{[...options, ...selectedOptions].map((option) => (
|
|
112
114
|
<SelectOption key={key(option)} value={key(option)}>
|
|
113
115
|
{isString(option) ? option : option.value}
|
|
114
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) => (
|
|
@@ -239,6 +239,7 @@ function DataTable<T>({
|
|
|
239
239
|
(v) => get(v, "id") === row.data.id,
|
|
240
240
|
),
|
|
241
241
|
variant: isRadio ? "radio" : "checkbox",
|
|
242
|
+
isDisabled: row.disableSelection,
|
|
242
243
|
}}
|
|
243
244
|
/>
|
|
244
245
|
)}
|
|
@@ -261,7 +262,7 @@ function DataTable<T>({
|
|
|
261
262
|
rows[index + 1].cells.length === 0
|
|
262
263
|
? undefined
|
|
263
264
|
: {
|
|
264
|
-
isExpanded:
|
|
265
|
+
isExpanded: expandedRows[index] ?? false,
|
|
265
266
|
rowIndex: index,
|
|
266
267
|
expandId: "expandable-row-",
|
|
267
268
|
onToggle: (_, rowIndex, isOpen) => {
|
|
@@ -281,7 +282,7 @@ function DataTable<T>({
|
|
|
281
282
|
/>
|
|
282
283
|
</Tr>
|
|
283
284
|
) : (
|
|
284
|
-
<Tr isExpanded={
|
|
285
|
+
<Tr isExpanded={expandedRows[index - 1] ?? false}>
|
|
285
286
|
<Td />
|
|
286
287
|
<Td colSpan={columns.length}>
|
|
287
288
|
<ExpandableRowContent>
|
|
@@ -360,7 +361,7 @@ export type DataListProps<T> = Omit<
|
|
|
360
361
|
* @param {DataListProps} props - The properties.
|
|
361
362
|
* @param {string} props.ariaLabelKey - The aria label key i18n key to lookup the label
|
|
362
363
|
* @param {string} props.searchPlaceholderKey - The i18n key to lookup the placeholder for the search box
|
|
363
|
-
* @param {boolean} props.isPaginated - if true the
|
|
364
|
+
* @param {boolean} props.isPaginated - if true, the loader will be called with first, max and search and a pager will be added in the header
|
|
364
365
|
* @param {(first?: number, max?: number, search?: string) => Promise<T[]>} props.loader - loader function that will fetch the data to display first, max and search are only applicable when isPaginated = true
|
|
365
366
|
* @param {Field<T>} props.columns - definition of the columns
|
|
366
367
|
* @param {Field<T>} props.detailColumns - definition of the columns expandable columns
|
|
@@ -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";
|
|
@@ -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}
|
|
@@ -22,7 +22,7 @@ export type KeycloakSelectProps = Omit<
|
|
|
22
22
|
"name" | "toggle" | "selected" | "onClick" | "onSelect" | "variant"
|
|
23
23
|
> & {
|
|
24
24
|
toggleId?: string;
|
|
25
|
-
onFilter?: (value: string) =>
|
|
25
|
+
onFilter?: (value: string) => void;
|
|
26
26
|
onClear?: () => void;
|
|
27
27
|
variant?: Variant;
|
|
28
28
|
isDisabled?: boolean;
|
|
@@ -33,6 +33,7 @@ export const MultiInputComponent = ({
|
|
|
33
33
|
form={form}
|
|
34
34
|
aria-label={labelAttribute(t, attribute)}
|
|
35
35
|
name={fieldName(attribute.name)!}
|
|
36
|
+
defaultValue={[attribute.defaultValue || ""]}
|
|
36
37
|
addButtonLabel={t("addMultivaluedLabel", {
|
|
37
38
|
fieldLabel: labelAttribute(t, attribute),
|
|
38
39
|
})}
|
|
@@ -31,7 +31,7 @@ export const OptionComponent = (props: UserProfileFieldProps) => {
|
|
|
31
31
|
<Controller
|
|
32
32
|
name={fieldName(attribute.name)}
|
|
33
33
|
control={form.control}
|
|
34
|
-
defaultValue=
|
|
34
|
+
defaultValue={attribute.defaultValue}
|
|
35
35
|
render={({ field }) => (
|
|
36
36
|
<>
|
|
37
37
|
{options.map((option) => (
|
|
@@ -41,15 +41,15 @@ export const OptionComponent = (props: UserProfileFieldProps) => {
|
|
|
41
41
|
data-testid={option}
|
|
42
42
|
label={label(props.t, optionLabel[option], option, prefix)}
|
|
43
43
|
value={option}
|
|
44
|
-
isChecked={field.value
|
|
44
|
+
isChecked={field.value?.includes(option)}
|
|
45
45
|
onChange={() => {
|
|
46
46
|
if (isMultiSelect) {
|
|
47
|
-
if (field.value
|
|
47
|
+
if (field.value?.includes(option)) {
|
|
48
48
|
field.onChange(
|
|
49
|
-
field.value
|
|
49
|
+
field.value?.filter((item: string) => item !== option),
|
|
50
50
|
);
|
|
51
51
|
} else {
|
|
52
|
-
field.onChange([...field.value, option]);
|
|
52
|
+
field.onChange([...(field.value || []), option]);
|
|
53
53
|
}
|
|
54
54
|
} else {
|
|
55
55
|
field.onChange([option]);
|
|
@@ -70,7 +70,7 @@ export const SelectComponent = (props: UserProfileFieldProps) => {
|
|
|
70
70
|
<UserProfileGroup {...props}>
|
|
71
71
|
<Controller
|
|
72
72
|
name={fieldName(attribute.name)}
|
|
73
|
-
defaultValue=
|
|
73
|
+
defaultValue={attribute.defaultValue}
|
|
74
74
|
control={form.control}
|
|
75
75
|
render={({ field }) => (
|
|
76
76
|
<KeycloakSelect
|
|
@@ -21,6 +21,7 @@ export const TextAreaComponent = (props: UserProfileFieldProps) => {
|
|
|
21
21
|
rows={attribute.annotations?.["inputTypeRows"] as number}
|
|
22
22
|
readOnly={attribute.readOnly}
|
|
23
23
|
isRequired={isRequired}
|
|
24
|
+
defaultValue={attribute.defaultValue}
|
|
24
25
|
/>
|
|
25
26
|
</UserProfileGroup>
|
|
26
27
|
);
|
|
@@ -33,8 +33,9 @@ export const TextComponent = (props: UserProfileFieldProps) => {
|
|
|
33
33
|
] as string,
|
|
34
34
|
)
|
|
35
35
|
}
|
|
36
|
-
|
|
36
|
+
isDisabled={attribute.readOnly}
|
|
37
37
|
isRequired={isRequired}
|
|
38
|
+
defaultValue={attribute.defaultValue}
|
|
38
39
|
{...form.register(fieldName(attribute.name))}
|
|
39
40
|
/>
|
|
40
41
|
</UserProfileGroup>
|
|
@@ -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,7 +81,7 @@ export function setUserProfileServerError<T>(
|
|
|
77
81
|
).forEach((e) => {
|
|
78
82
|
const params = Object.assign(
|
|
79
83
|
{},
|
|
80
|
-
e.params?.map((p) => (isBundleKey(p
|
|
84
|
+
e.params?.map((p) => (isBundleKey(p) ? t(unWrap(p as string)) : p)),
|
|
81
85
|
);
|
|
82
86
|
setError(fieldName(e.field) as keyof T, {
|
|
83
87
|
message: t(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@keycloakify/keycloak-ui-shared",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "260500.0.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git://github.com/keycloakify/keycloak-ui-shared.git"
|
|
@@ -13,13 +13,13 @@
|
|
|
13
13
|
"@patternfly/react-icons": "^5.4.2",
|
|
14
14
|
"@patternfly/react-styles": "^5.4.1",
|
|
15
15
|
"@patternfly/react-table": "^5.4.16",
|
|
16
|
-
"i18next": "^
|
|
17
|
-
"keycloak-js": "^26.2.
|
|
18
|
-
"lodash-es": "^4.17.
|
|
16
|
+
"i18next": "^25.7.3",
|
|
17
|
+
"keycloak-js": "^26.2.2",
|
|
18
|
+
"lodash-es": "^4.17.22",
|
|
19
19
|
"react": "^18.3.1",
|
|
20
|
-
"react-hook-form": "7.
|
|
21
|
-
"react-i18next": "^
|
|
22
|
-
"@keycloak/keycloak-admin-client": "26.
|
|
20
|
+
"react-hook-form": "7.70.0",
|
|
21
|
+
"react-i18next": "^16.5.1",
|
|
22
|
+
"@keycloak/keycloak-admin-client": "26.5.0",
|
|
23
23
|
"@types/lodash-es": "^4.17.12",
|
|
24
24
|
"@types/react": "^18.3.18"
|
|
25
25
|
},
|