@keycloakify/keycloak-ui-shared 260007.0.5 → 260200.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 +10 -6
- package/keycloak-theme/shared/keycloak-ui-shared/context/KeycloakContext.tsx +9 -2
- package/keycloak-theme/shared/keycloak-ui-shared/context/environment.ts +2 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/OrganizationTable.tsx +3 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/PasswordControl.tsx +2 -1
- package/keycloak-theme/shared/keycloak-ui-shared/controls/SwitchControl.tsx +2 -1
- package/keycloak-theme/shared/keycloak-ui-shared/controls/TextAreaControl.tsx +1 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/TextControl.tsx +3 -2
- package/keycloak-theme/shared/keycloak-ui-shared/controls/select-control/SelectControl.tsx +4 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/select-control/SingleSelectControl.tsx +13 -4
- package/keycloak-theme/shared/keycloak-ui-shared/controls/select-control/TypeaheadSelectControl.tsx +9 -8
- package/keycloak-theme/shared/keycloak-ui-shared/controls/table/KeycloakDataTable.tsx +134 -92
- package/keycloak-theme/shared/keycloak-ui-shared/controls/table/TableToolbar.tsx +7 -15
- package/keycloak-theme/shared/keycloak-ui-shared/icons/IconMapper.tsx +0 -1
- package/keycloak-theme/shared/keycloak-ui-shared/main.ts +0 -1
- package/keycloak-theme/shared/keycloak-ui-shared/masthead/Masthead.tsx +3 -0
- 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/SelectComponent.tsx +1 -1
- package/keycloak-theme/shared/keycloak-ui-shared/user-profile/TextComponent.tsx +12 -6
- package/keycloak-theme/shared/keycloak-ui-shared/user-profile/utils.ts +4 -28
- package/package.json +11 -11
|
@@ -9,9 +9,9 @@ import {
|
|
|
9
9
|
Page,
|
|
10
10
|
Text,
|
|
11
11
|
TextContent,
|
|
12
|
-
TextVariants,
|
|
13
12
|
} from "../../@patternfly/react-core";
|
|
14
13
|
import { useTranslation } from "react-i18next";
|
|
14
|
+
import { getNetworkErrorDescription } from "../utils/errors";
|
|
15
15
|
|
|
16
16
|
type ErrorPageProps = {
|
|
17
17
|
error?: unknown;
|
|
@@ -20,7 +20,10 @@ type ErrorPageProps = {
|
|
|
20
20
|
export const ErrorPage = (props: ErrorPageProps) => {
|
|
21
21
|
const { t } = useTranslation();
|
|
22
22
|
const error = props.error;
|
|
23
|
-
const errorMessage =
|
|
23
|
+
const errorMessage =
|
|
24
|
+
getErrorMessage(error) ||
|
|
25
|
+
getNetworkErrorDescription(error)?.replace(/\+/g, " ");
|
|
26
|
+
console.error(error);
|
|
24
27
|
|
|
25
28
|
function onRetry() {
|
|
26
29
|
location.href = location.origin + location.pathname;
|
|
@@ -30,7 +33,7 @@ export const ErrorPage = (props: ErrorPageProps) => {
|
|
|
30
33
|
<Page>
|
|
31
34
|
<Modal
|
|
32
35
|
variant={ModalVariant.small}
|
|
33
|
-
title={t("somethingWentWrong")}
|
|
36
|
+
title={errorMessage ? "" : t("somethingWentWrong")}
|
|
34
37
|
titleIconVariant="danger"
|
|
35
38
|
showClose={false}
|
|
36
39
|
isOpen
|
|
@@ -41,9 +44,10 @@ export const ErrorPage = (props: ErrorPageProps) => {
|
|
|
41
44
|
]}
|
|
42
45
|
>
|
|
43
46
|
<TextContent>
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
{errorMessage ? (
|
|
48
|
+
<Text>{t(errorMessage)}</Text>
|
|
49
|
+
) : (
|
|
50
|
+
<Text>{t("somethingWentWrongDescription")}</Text>
|
|
47
51
|
)}
|
|
48
52
|
</TextContent>
|
|
49
53
|
</Modal>
|
|
@@ -74,6 +74,7 @@ export const KeycloakProvider = <T extends BaseEnvironment>({
|
|
|
74
74
|
onLoad: "check-sso",
|
|
75
75
|
pkceMethod: "S256",
|
|
76
76
|
responseMode: "query",
|
|
77
|
+
scope: environment.scope,
|
|
77
78
|
});
|
|
78
79
|
|
|
79
80
|
init()
|
|
@@ -83,8 +84,14 @@ export const KeycloakProvider = <T extends BaseEnvironment>({
|
|
|
83
84
|
calledOnce.current = true;
|
|
84
85
|
}, [keycloak]);
|
|
85
86
|
|
|
86
|
-
|
|
87
|
-
|
|
87
|
+
const searchParams = new URLSearchParams(window.location.search);
|
|
88
|
+
|
|
89
|
+
if (error || searchParams.get("error_description")) {
|
|
90
|
+
return (
|
|
91
|
+
<ErrorPage
|
|
92
|
+
error={error ? error : searchParams.get("error_description")}
|
|
93
|
+
/>
|
|
94
|
+
);
|
|
88
95
|
}
|
|
89
96
|
|
|
90
97
|
if (!init) {
|
|
@@ -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}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
UseControllerProps,
|
|
16
16
|
useController,
|
|
17
17
|
} from "react-hook-form";
|
|
18
|
+
import { getRuleValue } from "../utils/getRuleValue";
|
|
18
19
|
import { FormLabel } from "./FormLabel";
|
|
19
20
|
import { PasswordInput, PasswordInputProps } from "./PasswordInput";
|
|
20
21
|
|
|
@@ -36,7 +37,7 @@ export const PasswordControl = <
|
|
|
36
37
|
props: PasswordControlProps<T, P>,
|
|
37
38
|
) => {
|
|
38
39
|
const { labelIcon, ...rest } = props;
|
|
39
|
-
const required = !!props.rules?.required;
|
|
40
|
+
const required = !!getRuleValue(props.rules?.required);
|
|
40
41
|
const defaultValue = props.defaultValue ?? ("" as PathValue<T, P>);
|
|
41
42
|
|
|
42
43
|
const { field, fieldState } = useController({
|
|
@@ -12,6 +12,7 @@ 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,
|
|
@@ -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) => {
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
UseControllerProps,
|
|
19
19
|
useController,
|
|
20
20
|
} from "react-hook-form";
|
|
21
|
-
|
|
21
|
+
import { getRuleValue } from "../utils/getRuleValue";
|
|
22
22
|
import { FormLabel } from "./FormLabel";
|
|
23
23
|
|
|
24
24
|
export type TextControlProps<
|
|
@@ -31,6 +31,7 @@ export type TextControlProps<
|
|
|
31
31
|
isDisabled?: boolean;
|
|
32
32
|
helperText?: string;
|
|
33
33
|
"data-testid"?: string;
|
|
34
|
+
type?: string;
|
|
34
35
|
};
|
|
35
36
|
|
|
36
37
|
export const TextControl = <
|
|
@@ -40,7 +41,7 @@ export const TextControl = <
|
|
|
40
41
|
props: TextControlProps<T, P>,
|
|
41
42
|
) => {
|
|
42
43
|
const { labelIcon, helperText, ...rest } = props;
|
|
43
|
-
const required = !!props.rules?.required;
|
|
44
|
+
const required = !!getRuleValue(props.rules?.required);
|
|
44
45
|
const defaultValue = props.defaultValue ?? ("" as PathValue<T, P>);
|
|
45
46
|
|
|
46
47
|
const { field, fieldState } = useController({
|
|
@@ -53,6 +53,10 @@ export type SelectControlProps<
|
|
|
53
53
|
menuAppendTo?: string;
|
|
54
54
|
placeholderText?: string;
|
|
55
55
|
chipGroupProps?: ChipGroupProps;
|
|
56
|
+
onSelect?: (
|
|
57
|
+
value: string | string[],
|
|
58
|
+
onChangeHandler: (value: string | string[]) => void,
|
|
59
|
+
) => void;
|
|
56
60
|
};
|
|
57
61
|
|
|
58
62
|
export const isSelectBasedOptions = (
|
package/keycloak-theme/shared/keycloak-ui-shared/controls/select-control/SingleSelectControl.tsx
CHANGED
|
@@ -36,6 +36,8 @@ export const SingleSelectControl = <
|
|
|
36
36
|
options,
|
|
37
37
|
controller,
|
|
38
38
|
labelIcon,
|
|
39
|
+
isDisabled,
|
|
40
|
+
onSelect,
|
|
39
41
|
...rest
|
|
40
42
|
}: SelectControlProps<T, P>) => {
|
|
41
43
|
const {
|
|
@@ -60,6 +62,7 @@ export const SingleSelectControl = <
|
|
|
60
62
|
render={({ field: { onChange, value } }) => (
|
|
61
63
|
<Select
|
|
62
64
|
{...rest}
|
|
65
|
+
variant="default"
|
|
63
66
|
onClick={() => setOpen(!open)}
|
|
64
67
|
onOpenChange={() => setOpen(false)}
|
|
65
68
|
selected={
|
|
@@ -81,7 +84,8 @@ export const SingleSelectControl = <
|
|
|
81
84
|
isExpanded={open}
|
|
82
85
|
isFullWidth
|
|
83
86
|
status={get(errors, name) ? MenuToggleStatus.danger : undefined}
|
|
84
|
-
aria-label=
|
|
87
|
+
aria-label={label}
|
|
88
|
+
isDisabled={isDisabled}
|
|
85
89
|
>
|
|
86
90
|
{isSelectBasedOptions(options)
|
|
87
91
|
? options.find(
|
|
@@ -92,13 +96,18 @@ export const SingleSelectControl = <
|
|
|
92
96
|
</MenuToggle>
|
|
93
97
|
)}
|
|
94
98
|
onSelect={(_event, v) => {
|
|
95
|
-
const option = v?.toString()
|
|
96
|
-
|
|
99
|
+
const option = v?.toString()!;
|
|
100
|
+
const convertedValue = Array.isArray(value) ? [option] : option;
|
|
101
|
+
if (onSelect) {
|
|
102
|
+
onSelect(convertedValue, onChange);
|
|
103
|
+
} else {
|
|
104
|
+
onChange(convertedValue);
|
|
105
|
+
}
|
|
97
106
|
setOpen(false);
|
|
98
107
|
}}
|
|
99
108
|
isOpen={open}
|
|
100
109
|
>
|
|
101
|
-
<SelectList>
|
|
110
|
+
<SelectList data-testid={`select-${name}`}>
|
|
102
111
|
{options.map((option) => (
|
|
103
112
|
<SelectOption key={key(option)} value={key(option)}>
|
|
104
113
|
{isString(option) ? option : option.value}
|
package/keycloak-theme/shared/keycloak-ui-shared/controls/select-control/TypeaheadSelectControl.tsx
CHANGED
|
@@ -63,6 +63,7 @@ export const TypeaheadSelectControl = <
|
|
|
63
63
|
const [focusedItemIndex, setFocusedItemIndex] = useState<number>(0);
|
|
64
64
|
const textInputRef = useRef<HTMLInputElement>();
|
|
65
65
|
const required = getRuleValue(controller.rules?.required) === true;
|
|
66
|
+
const isTypeaheadMulti = variant === SelectVariant.typeaheadMulti;
|
|
66
67
|
|
|
67
68
|
const filteredOptions = options.filter((option) =>
|
|
68
69
|
getValue(option).toLowerCase().startsWith(filterValue.toLowerCase()),
|
|
@@ -93,7 +94,7 @@ export const TypeaheadSelectControl = <
|
|
|
93
94
|
case "Enter": {
|
|
94
95
|
event.preventDefault();
|
|
95
96
|
|
|
96
|
-
if (
|
|
97
|
+
if (!isTypeaheadMulti) {
|
|
97
98
|
setFilterValue(getValue(focusedItem));
|
|
98
99
|
} else {
|
|
99
100
|
setFilterValue("");
|
|
@@ -164,7 +165,6 @@ export const TypeaheadSelectControl = <
|
|
|
164
165
|
render={({ field }) => (
|
|
165
166
|
<Select
|
|
166
167
|
{...rest}
|
|
167
|
-
onClick={() => setOpen(!open)}
|
|
168
168
|
onOpenChange={() => setOpen(false)}
|
|
169
169
|
selected={
|
|
170
170
|
isSelectBasedOptions(options)
|
|
@@ -177,12 +177,16 @@ export const TypeaheadSelectControl = <
|
|
|
177
177
|
.map((o) => o.value)
|
|
178
178
|
: field.value
|
|
179
179
|
}
|
|
180
|
+
shouldFocusFirstItemOnOpen={false}
|
|
180
181
|
toggle={(ref) => (
|
|
181
182
|
<MenuToggle
|
|
182
183
|
ref={ref}
|
|
183
184
|
id={id || name.slice(name.lastIndexOf(".") + 1)}
|
|
184
185
|
variant="typeahead"
|
|
185
|
-
onClick={() =>
|
|
186
|
+
onClick={() => {
|
|
187
|
+
setOpen(!open);
|
|
188
|
+
textInputRef.current?.focus();
|
|
189
|
+
}}
|
|
186
190
|
isExpanded={open}
|
|
187
191
|
isFullWidth
|
|
188
192
|
status={get(errors, name) ? MenuToggleStatus.danger : undefined}
|
|
@@ -247,7 +251,7 @@ export const TypeaheadSelectControl = <
|
|
|
247
251
|
variant="plain"
|
|
248
252
|
onClick={() => {
|
|
249
253
|
setFilterValue("");
|
|
250
|
-
field.onChange("");
|
|
254
|
+
field.onChange(isTypeaheadMulti ? [] : "");
|
|
251
255
|
textInputRef?.current?.focus();
|
|
252
256
|
}}
|
|
253
257
|
aria-label="Clear input value"
|
|
@@ -262,10 +266,7 @@ export const TypeaheadSelectControl = <
|
|
|
262
266
|
onSelect={(event, v) => {
|
|
263
267
|
event?.stopPropagation();
|
|
264
268
|
const option = v?.toString();
|
|
265
|
-
if (
|
|
266
|
-
variant === SelectVariant.typeaheadMulti &&
|
|
267
|
-
Array.isArray(field.value)
|
|
268
|
-
) {
|
|
269
|
+
if (isTypeaheadMulti && Array.isArray(field.value)) {
|
|
269
270
|
if (field.value.includes(option)) {
|
|
270
271
|
field.onChange(
|
|
271
272
|
field.value.filter((item: string) => item !== option),
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// @ts-nocheck
|
|
4
4
|
|
|
5
5
|
import { Button, ButtonVariant, ToolbarItem } from "../../../@patternfly/react-core";
|
|
6
|
+
import { SyncAltIcon } from "../../../@patternfly/react-icons";
|
|
6
7
|
import type { SVGIconProps } from "@patternfly/react-icons/dist/js/createIcon";
|
|
7
8
|
import {
|
|
8
9
|
ActionsColumn,
|
|
@@ -23,7 +24,7 @@ import {
|
|
|
23
24
|
Thead,
|
|
24
25
|
Tr,
|
|
25
26
|
} from "../../../@patternfly/react-table";
|
|
26
|
-
import { cloneDeep,
|
|
27
|
+
import { cloneDeep, get, intersectionBy } from "lodash-es";
|
|
27
28
|
import {
|
|
28
29
|
ComponentClass,
|
|
29
30
|
ReactNode,
|
|
@@ -36,13 +37,11 @@ import {
|
|
|
36
37
|
type JSX,
|
|
37
38
|
} from "react";
|
|
38
39
|
import { useTranslation } from "react-i18next";
|
|
39
|
-
|
|
40
|
-
import { useStoredState } from "../../utils/useStoredState";
|
|
41
40
|
import { useFetch } from "../../utils/useFetch";
|
|
41
|
+
import { useStoredState } from "../../utils/useStoredState";
|
|
42
|
+
import { KeycloakSpinner } from "../KeycloakSpinner";
|
|
42
43
|
import { ListEmptyState } from "./ListEmptyState";
|
|
43
44
|
import { PaginatingTableToolbar } from "./PaginatingTableToolbar";
|
|
44
|
-
import { SyncAltIcon } from "../../../@patternfly/react-icons";
|
|
45
|
-
import { KeycloakSpinner } from "../KeycloakSpinner";
|
|
46
45
|
|
|
47
46
|
type TitleCell = { title: JSX.Element };
|
|
48
47
|
type Cell<T> = keyof T | JSX.Element | TitleCell;
|
|
@@ -69,24 +68,50 @@ type DataTableProps<T> = {
|
|
|
69
68
|
rows: (Row<T> | SubRow<T>)[];
|
|
70
69
|
actions?: IActions;
|
|
71
70
|
actionResolver?: IActionsResolver;
|
|
72
|
-
|
|
71
|
+
selected?: T[];
|
|
72
|
+
onSelect?: (value: T[]) => void;
|
|
73
73
|
onCollapse?: (isOpen: boolean, rowIndex: number) => void;
|
|
74
74
|
canSelectAll: boolean;
|
|
75
|
+
canSelect: boolean;
|
|
75
76
|
isNotCompact?: boolean;
|
|
76
77
|
isRadio?: boolean;
|
|
77
78
|
};
|
|
78
79
|
|
|
79
80
|
type CellRendererProps = {
|
|
80
81
|
row: IRow;
|
|
82
|
+
index?: number;
|
|
83
|
+
actions?: IActions;
|
|
84
|
+
actionResolver?: IActionsResolver;
|
|
81
85
|
};
|
|
82
86
|
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
87
|
+
const isRow = (c: ReactNode | IRowCell): c is IRowCell =>
|
|
88
|
+
!!c && (c as IRowCell).title !== undefined;
|
|
89
|
+
|
|
90
|
+
const CellRenderer = ({
|
|
91
|
+
row,
|
|
92
|
+
index,
|
|
93
|
+
actions,
|
|
94
|
+
actionResolver,
|
|
95
|
+
}: CellRendererProps) => (
|
|
96
|
+
<>
|
|
97
|
+
{row.cells!.map((c, i) => (
|
|
98
|
+
<Td key={`cell-${i}`}>{(isRow(c) ? c.title : c) as ReactNode}</Td>
|
|
99
|
+
))}
|
|
100
|
+
{(actions || actionResolver) && (
|
|
101
|
+
<Td isActionCell>
|
|
102
|
+
<ActionsColumn
|
|
103
|
+
items={actions || actionResolver?.(row, {})!}
|
|
104
|
+
extraData={{ rowIndex: index }}
|
|
105
|
+
/>
|
|
106
|
+
</Td>
|
|
107
|
+
)}
|
|
108
|
+
</>
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const ExpandableRowRenderer = ({ row }: CellRendererProps) =>
|
|
112
|
+
row.cells!.map((c, i) => (
|
|
113
|
+
<div key={`cell-${i}`}>{(isRow(c) ? c.title : c) as ReactNode}</div>
|
|
88
114
|
));
|
|
89
|
-
};
|
|
90
115
|
|
|
91
116
|
function DataTable<T>({
|
|
92
117
|
columns,
|
|
@@ -94,37 +119,75 @@ function DataTable<T>({
|
|
|
94
119
|
actions,
|
|
95
120
|
actionResolver,
|
|
96
121
|
ariaLabelKey,
|
|
122
|
+
selected,
|
|
97
123
|
onSelect,
|
|
98
124
|
onCollapse,
|
|
99
125
|
canSelectAll,
|
|
126
|
+
canSelect,
|
|
100
127
|
isNotCompact,
|
|
101
128
|
isRadio,
|
|
102
129
|
...props
|
|
103
130
|
}: DataTableProps<T>) {
|
|
104
131
|
const { t } = useTranslation();
|
|
105
|
-
|
|
106
|
-
const [selectedRows, setSelectedRows] = useState<boolean[]>([]);
|
|
132
|
+
const [selectedRows, setSelectedRows] = useState<T[]>(selected || []);
|
|
107
133
|
const [expandedRows, setExpandedRows] = useState<boolean[]>([]);
|
|
108
134
|
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
135
|
+
const rowsSelectedOnPage = useMemo(
|
|
136
|
+
() =>
|
|
137
|
+
intersectionBy(
|
|
138
|
+
selectedRows,
|
|
139
|
+
rows.map((row) => row.data),
|
|
140
|
+
"id",
|
|
141
|
+
),
|
|
142
|
+
[selectedRows, rows],
|
|
143
|
+
);
|
|
116
144
|
|
|
117
145
|
useEffect(() => {
|
|
118
146
|
if (canSelectAll) {
|
|
119
147
|
const selectAllCheckbox = document.getElementsByName("check-all").item(0);
|
|
120
148
|
if (selectAllCheckbox) {
|
|
121
149
|
const checkbox = selectAllCheckbox as HTMLInputElement;
|
|
122
|
-
const selected = selectedRows.filter((r) => r === true);
|
|
123
150
|
checkbox.indeterminate =
|
|
124
|
-
|
|
151
|
+
rowsSelectedOnPage.length < rows.length &&
|
|
152
|
+
rowsSelectedOnPage.length > 0;
|
|
125
153
|
}
|
|
126
154
|
}
|
|
127
|
-
}, [selectedRows]);
|
|
155
|
+
}, [selectedRows, canSelectAll, rows]);
|
|
156
|
+
|
|
157
|
+
const updateSelectedRows = (selected: T[]) => {
|
|
158
|
+
setSelectedRows(selected);
|
|
159
|
+
onSelect?.(selected);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const updateState = (rowIndex: number, isSelected: boolean) => {
|
|
163
|
+
if (isRadio) {
|
|
164
|
+
const selectedRow = isSelected ? [rows[rowIndex].data] : [];
|
|
165
|
+
updateSelectedRows(selectedRow);
|
|
166
|
+
} else {
|
|
167
|
+
if (rowIndex === -1) {
|
|
168
|
+
const rowsSelectedOnPageIds = rowsSelectedOnPage.map((v) =>
|
|
169
|
+
get(v, "id"),
|
|
170
|
+
);
|
|
171
|
+
updateSelectedRows(
|
|
172
|
+
isSelected
|
|
173
|
+
? [...selectedRows, ...rows.map((row) => row.data)]
|
|
174
|
+
: selectedRows.filter(
|
|
175
|
+
(v) => !rowsSelectedOnPageIds.includes(get(v, "id")),
|
|
176
|
+
),
|
|
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
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
128
191
|
|
|
129
192
|
return (
|
|
130
193
|
<Table
|
|
@@ -134,19 +197,17 @@ function DataTable<T>({
|
|
|
134
197
|
>
|
|
135
198
|
<Thead>
|
|
136
199
|
<Tr>
|
|
137
|
-
{onCollapse && <Th />}
|
|
200
|
+
{onCollapse && <Th screenReaderText={t("expandRow")} />}
|
|
138
201
|
{canSelectAll && (
|
|
139
202
|
<Th
|
|
203
|
+
screenReaderText={t("selectAll")}
|
|
140
204
|
select={
|
|
141
205
|
!isRadio
|
|
142
206
|
? {
|
|
143
|
-
onSelect: (_, isSelected
|
|
144
|
-
onSelect!(isSelected, rowIndex);
|
|
207
|
+
onSelect: (_, isSelected) => {
|
|
145
208
|
updateState(-1, isSelected);
|
|
146
209
|
},
|
|
147
|
-
isSelected:
|
|
148
|
-
selectedRows.filter((r) => r === true).length ===
|
|
149
|
-
rows.length,
|
|
210
|
+
isSelected: rowsSelectedOnPage.length === rows.length,
|
|
150
211
|
}
|
|
151
212
|
: undefined
|
|
152
213
|
}
|
|
@@ -154,7 +215,8 @@ function DataTable<T>({
|
|
|
154
215
|
)}
|
|
155
216
|
{columns.map((column) => (
|
|
156
217
|
<Th
|
|
157
|
-
|
|
218
|
+
screenReaderText={t("expandRow")}
|
|
219
|
+
key={column.displayKey || column.name}
|
|
158
220
|
className={column.transforms?.[0]().className}
|
|
159
221
|
>
|
|
160
222
|
{t(column.displayKey || column.name)}
|
|
@@ -166,28 +228,26 @@ function DataTable<T>({
|
|
|
166
228
|
<Tbody>
|
|
167
229
|
{(rows as IRow[]).map((row, index) => (
|
|
168
230
|
<Tr key={index} isExpanded={expandedRows[index]}>
|
|
169
|
-
{
|
|
231
|
+
{canSelect && (
|
|
170
232
|
<Td
|
|
171
233
|
select={{
|
|
172
234
|
rowIndex: index,
|
|
173
235
|
onSelect: (_, isSelected, rowIndex) => {
|
|
174
|
-
onSelect!(isSelected, rowIndex);
|
|
175
236
|
updateState(rowIndex, isSelected);
|
|
176
237
|
},
|
|
177
|
-
isSelected: selectedRows
|
|
238
|
+
isSelected: !!selectedRows.find(
|
|
239
|
+
(v) => get(v, "id") === row.data.id,
|
|
240
|
+
),
|
|
178
241
|
variant: isRadio ? "radio" : "checkbox",
|
|
179
242
|
}}
|
|
180
243
|
/>
|
|
181
244
|
)}
|
|
182
|
-
<CellRenderer
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
/>
|
|
189
|
-
</Td>
|
|
190
|
-
)}
|
|
245
|
+
<CellRenderer
|
|
246
|
+
row={row}
|
|
247
|
+
index={index}
|
|
248
|
+
actions={actions}
|
|
249
|
+
actionResolver={actionResolver}
|
|
250
|
+
/>
|
|
191
251
|
</Tr>
|
|
192
252
|
))}
|
|
193
253
|
</Tbody>
|
|
@@ -197,26 +257,35 @@ function DataTable<T>({
|
|
|
197
257
|
{index % 2 === 0 ? (
|
|
198
258
|
<Tr>
|
|
199
259
|
<Td
|
|
200
|
-
expand={
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
260
|
+
expand={
|
|
261
|
+
rows[index + 1].cells.length === 0
|
|
262
|
+
? undefined
|
|
263
|
+
: {
|
|
264
|
+
isExpanded: !!expandedRows[index],
|
|
265
|
+
rowIndex: index,
|
|
266
|
+
expandId: "expandable-row-",
|
|
267
|
+
onToggle: (_, rowIndex, isOpen) => {
|
|
268
|
+
onCollapse(isOpen, rowIndex);
|
|
269
|
+
const expand = [...expandedRows];
|
|
270
|
+
expand[index] = isOpen;
|
|
271
|
+
setExpandedRows(expand);
|
|
272
|
+
},
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/>
|
|
276
|
+
<CellRenderer
|
|
277
|
+
row={row}
|
|
278
|
+
index={index}
|
|
279
|
+
actions={actions}
|
|
280
|
+
actionResolver={actionResolver}
|
|
211
281
|
/>
|
|
212
|
-
<CellRenderer row={row} />
|
|
213
282
|
</Tr>
|
|
214
283
|
) : (
|
|
215
284
|
<Tr isExpanded={!!expandedRows[index - 1]}>
|
|
216
285
|
<Td />
|
|
217
286
|
<Td colSpan={columns.length}>
|
|
218
287
|
<ExpandableRowContent>
|
|
219
|
-
<
|
|
288
|
+
<ExpandableRowRenderer row={row} />
|
|
220
289
|
</ExpandableRowContent>
|
|
221
290
|
</Td>
|
|
222
291
|
</Tr>
|
|
@@ -480,38 +549,6 @@ export function KeycloakDataTable<T>({
|
|
|
480
549
|
return action;
|
|
481
550
|
});
|
|
482
551
|
|
|
483
|
-
const _onSelect = (isSelected: boolean, rowIndex: number) => {
|
|
484
|
-
const data = filteredData || rows;
|
|
485
|
-
if (rowIndex === -1) {
|
|
486
|
-
setRows(
|
|
487
|
-
data!.map((row) => {
|
|
488
|
-
(row as Row<T>).selected = isSelected;
|
|
489
|
-
return row;
|
|
490
|
-
}),
|
|
491
|
-
);
|
|
492
|
-
} else {
|
|
493
|
-
(data![rowIndex] as Row<T>).selected = isSelected;
|
|
494
|
-
|
|
495
|
-
setRows([...rows!]);
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
// Keeps selected items when paginating
|
|
499
|
-
const difference = differenceBy(
|
|
500
|
-
selected,
|
|
501
|
-
data!.map((row) => row.data),
|
|
502
|
-
"id",
|
|
503
|
-
);
|
|
504
|
-
|
|
505
|
-
// Selected rows are any rows previously selected from a different page, plus current page selections
|
|
506
|
-
const selectedRows = [
|
|
507
|
-
...difference,
|
|
508
|
-
...data!.filter((row) => (row as Row<T>).selected).map((row) => row.data),
|
|
509
|
-
];
|
|
510
|
-
|
|
511
|
-
setSelected(selectedRows);
|
|
512
|
-
onSelect!(selectedRows);
|
|
513
|
-
};
|
|
514
|
-
|
|
515
552
|
const onCollapse = (isOpen: boolean, rowIndex: number) => {
|
|
516
553
|
(data![rowIndex] as Row<T>).isOpen = isOpen;
|
|
517
554
|
setRows([...data!]);
|
|
@@ -526,7 +563,7 @@ export function KeycloakDataTable<T>({
|
|
|
526
563
|
|
|
527
564
|
return (
|
|
528
565
|
<>
|
|
529
|
-
{(
|
|
566
|
+
{(!noData || searching) && (
|
|
530
567
|
<PaginatingTableToolbar
|
|
531
568
|
id={id}
|
|
532
569
|
count={rowLength}
|
|
@@ -549,7 +586,7 @@ export function KeycloakDataTable<T>({
|
|
|
549
586
|
<>
|
|
550
587
|
{toolbarItem} <ToolbarItem variant="separator" />{" "}
|
|
551
588
|
<ToolbarItem>
|
|
552
|
-
<Button variant="link" onClick={refresh}>
|
|
589
|
+
<Button variant="link" onClick={refresh} data-testid="refresh">
|
|
553
590
|
<SyncAltIcon /> {t("refresh")}
|
|
554
591
|
</Button>
|
|
555
592
|
</ToolbarItem>
|
|
@@ -561,7 +598,12 @@ export function KeycloakDataTable<T>({
|
|
|
561
598
|
<DataTable
|
|
562
599
|
{...props}
|
|
563
600
|
canSelectAll={canSelectAll}
|
|
564
|
-
|
|
601
|
+
canSelect={!!onSelect}
|
|
602
|
+
selected={selected}
|
|
603
|
+
onSelect={(selected) => {
|
|
604
|
+
setSelected(selected);
|
|
605
|
+
onSelect?.(selected);
|
|
606
|
+
}}
|
|
565
607
|
onCollapse={detailColumns ? onCollapse : undefined}
|
|
566
608
|
actions={convertAction()}
|
|
567
609
|
actionResolver={actionResolver}
|
|
@@ -592,9 +634,9 @@ export function KeycloakDataTable<T>({
|
|
|
592
634
|
}
|
|
593
635
|
/>
|
|
594
636
|
)}
|
|
595
|
-
{loading && <KeycloakSpinner />}
|
|
596
637
|
</PaginatingTableToolbar>
|
|
597
638
|
)}
|
|
639
|
+
{loading && <KeycloakSpinner />}
|
|
598
640
|
{!loading && noData && !searching && emptyState}
|
|
599
641
|
</>
|
|
600
642
|
);
|
|
@@ -36,25 +36,20 @@ export const TableToolbar = ({
|
|
|
36
36
|
const { t } = useTranslation();
|
|
37
37
|
const [searchValue, setSearchValue] = useState<string>("");
|
|
38
38
|
|
|
39
|
-
const onSearch = () => {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
inputGroupOnEnter?.(searchValue);
|
|
43
|
-
} else {
|
|
44
|
-
setSearchValue("");
|
|
45
|
-
inputGroupOnEnter?.("");
|
|
46
|
-
}
|
|
39
|
+
const onSearch = (searchValue: string) => {
|
|
40
|
+
setSearchValue(searchValue.trim());
|
|
41
|
+
inputGroupOnEnter?.(searchValue.trim());
|
|
47
42
|
};
|
|
48
43
|
|
|
49
44
|
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
|
50
45
|
if (e.key === "Enter") {
|
|
51
|
-
onSearch();
|
|
46
|
+
onSearch(searchValue);
|
|
52
47
|
}
|
|
53
48
|
};
|
|
54
49
|
|
|
55
50
|
return (
|
|
56
51
|
<>
|
|
57
|
-
<Toolbar>
|
|
52
|
+
<Toolbar data-testid="table-toolbar">
|
|
58
53
|
<ToolbarContent>
|
|
59
54
|
{inputGroupName && (
|
|
60
55
|
<ToolbarItem>
|
|
@@ -69,12 +64,9 @@ export const TableToolbar = ({
|
|
|
69
64
|
onChange={(_, value) => {
|
|
70
65
|
setSearchValue(value);
|
|
71
66
|
}}
|
|
72
|
-
onSearch={onSearch}
|
|
67
|
+
onSearch={() => onSearch(searchValue)}
|
|
73
68
|
onKeyDown={handleKeyDown}
|
|
74
|
-
onClear={() =>
|
|
75
|
-
setSearchValue("");
|
|
76
|
-
inputGroupOnEnter?.("");
|
|
77
|
-
}}
|
|
69
|
+
onClear={() => onSearch("")}
|
|
78
70
|
/>
|
|
79
71
|
)}
|
|
80
72
|
</InputGroup>
|
|
@@ -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) => (
|
|
@@ -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>
|
|
@@ -21,12 +21,18 @@ export const TextComponent = (props: UserProfileFieldProps) => {
|
|
|
21
21
|
id={attribute.name}
|
|
22
22
|
data-testid={attribute.name}
|
|
23
23
|
type={type}
|
|
24
|
-
placeholder={
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
placeholder={
|
|
25
|
+
attribute.readOnly
|
|
26
|
+
? ""
|
|
27
|
+
: label(
|
|
28
|
+
props.t,
|
|
29
|
+
attribute.annotations?.["inputTypePlaceholder"] as string,
|
|
30
|
+
"",
|
|
31
|
+
attribute.annotations?.[
|
|
32
|
+
"inputOptionLabelsI18nPrefix"
|
|
33
|
+
] as string,
|
|
34
|
+
)
|
|
35
|
+
}
|
|
30
36
|
readOnly={attribute.readOnly}
|
|
31
37
|
isRequired={isRequired}
|
|
32
38
|
{...form.register(fieldName(attribute.name))}
|
|
@@ -77,12 +77,13 @@ export function setUserProfileServerError<T>(
|
|
|
77
77
|
).forEach((e) => {
|
|
78
78
|
const params = Object.assign(
|
|
79
79
|
{},
|
|
80
|
-
e.params?.map((p) => (isBundleKey(p
|
|
80
|
+
e.params?.map((p) => (isBundleKey(p?.toString()) ? t(unWrap(p)) : p)),
|
|
81
81
|
);
|
|
82
82
|
setError(fieldName(e.field) as keyof T, {
|
|
83
83
|
message: t(
|
|
84
84
|
isBundleKey(e.errorMessage) ? unWrap(e.errorMessage) : e.errorMessage,
|
|
85
85
|
{
|
|
86
|
+
/* eslint-disable @typescript-eslint/no-misused-spread */
|
|
86
87
|
...params,
|
|
87
88
|
defaultValue: e.errorMessage || e.field,
|
|
88
89
|
},
|
|
@@ -94,34 +95,9 @@ export function setUserProfileServerError<T>(
|
|
|
94
95
|
|
|
95
96
|
export function isRequiredAttribute({
|
|
96
97
|
required,
|
|
97
|
-
validators,
|
|
98
98
|
}: UserProfileAttributeMetadata): boolean {
|
|
99
|
-
// Check if required is true
|
|
100
|
-
return required
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Checks whether the given validators include a validation that would make the attribute implicitly required.
|
|
105
|
-
*/
|
|
106
|
-
function hasRequiredValidators(
|
|
107
|
-
validators?: UserProfileAttributeMetadata["validators"],
|
|
108
|
-
): boolean {
|
|
109
|
-
// If we don't have any validators, the attribute is not required.
|
|
110
|
-
if (!validators) {
|
|
111
|
-
return false;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// If the 'length' validator is defined and has a minimal length greater than zero the attribute is implicitly required.
|
|
115
|
-
// We have to do a lot of defensive coding here, because we don't have type information for the validators.
|
|
116
|
-
if (
|
|
117
|
-
"length" in validators &&
|
|
118
|
-
"min" in validators.length &&
|
|
119
|
-
typeof validators.length.min === "number"
|
|
120
|
-
) {
|
|
121
|
-
return validators.length.min > 0;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return false;
|
|
99
|
+
// Check if required is true
|
|
100
|
+
return required as boolean;
|
|
125
101
|
}
|
|
126
102
|
|
|
127
103
|
export function isUserProfileError(error: unknown): error is UserProfileError {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@keycloakify/keycloak-ui-shared",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "260200.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.
|
|
13
|
-
"@patternfly/react-icons": "^5.4.
|
|
14
|
-
"@patternfly/react-styles": "^5.4.
|
|
15
|
-
"@patternfly/react-table": "^5.4.
|
|
16
|
-
"i18next": "^
|
|
12
|
+
"@patternfly/react-core": "^5.4.14",
|
|
13
|
+
"@patternfly/react-icons": "^5.4.2",
|
|
14
|
+
"@patternfly/react-styles": "^5.4.1",
|
|
15
|
+
"@patternfly/react-table": "^5.4.16",
|
|
16
|
+
"i18next": "^24.2.3",
|
|
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.0
|
|
22
|
-
"keycloak-js": "26.0.7",
|
|
20
|
+
"react-hook-form": "7.54.2",
|
|
21
|
+
"react-i18next": "^15.4.1",
|
|
22
|
+
"@keycloak/keycloak-admin-client": "26.2.0",
|
|
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"
|