@keycloakify/keycloak-ui-shared 260007.0.5 → 260103.0.1
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/PasswordControl.tsx +2 -1
- package/keycloak-theme/shared/keycloak-ui-shared/controls/TextControl.tsx +3 -2
- package/keycloak-theme/shared/keycloak-ui-shared/controls/select-control/SingleSelectControl.tsx +1 -1
- 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 +109 -78
- package/keycloak-theme/shared/keycloak-ui-shared/controls/table/TableToolbar.tsx +6 -14
- 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/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 +2 -27
- 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) {
|
|
@@ -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({
|
|
@@ -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({
|
package/keycloak-theme/shared/keycloak-ui-shared/controls/select-control/SingleSelectControl.tsx
CHANGED
|
@@ -98,7 +98,7 @@ export const SingleSelectControl = <
|
|
|
98
98
|
}}
|
|
99
99
|
isOpen={open}
|
|
100
100
|
>
|
|
101
|
-
<SelectList>
|
|
101
|
+
<SelectList data-testid={`select-${name}`}>
|
|
102
102
|
{options.map((option) => (
|
|
103
103
|
<SelectOption key={key(option)} value={key(option)}>
|
|
104
104
|
{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,68 @@ 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 (rowIndex === -1) {
|
|
164
|
+
const rowsSelectedOnPageIds = rowsSelectedOnPage.map((v) => get(v, "id"));
|
|
165
|
+
updateSelectedRows(
|
|
166
|
+
isSelected
|
|
167
|
+
? [...selectedRows, ...rows.map((row) => row.data)]
|
|
168
|
+
: selectedRows.filter(
|
|
169
|
+
(v) => !rowsSelectedOnPageIds.includes(get(v, "id")),
|
|
170
|
+
),
|
|
171
|
+
);
|
|
172
|
+
} else {
|
|
173
|
+
if (isSelected) {
|
|
174
|
+
updateSelectedRows([...selectedRows, rows[rowIndex].data]);
|
|
175
|
+
} else {
|
|
176
|
+
updateSelectedRows(
|
|
177
|
+
selectedRows.filter(
|
|
178
|
+
(v) => get(v, "id") !== (rows[rowIndex] as IRow).data.id,
|
|
179
|
+
),
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
};
|
|
128
184
|
|
|
129
185
|
return (
|
|
130
186
|
<Table
|
|
@@ -134,19 +190,17 @@ function DataTable<T>({
|
|
|
134
190
|
>
|
|
135
191
|
<Thead>
|
|
136
192
|
<Tr>
|
|
137
|
-
{onCollapse && <Th />}
|
|
193
|
+
{onCollapse && <Th screenReaderText={t("expandRow")} />}
|
|
138
194
|
{canSelectAll && (
|
|
139
195
|
<Th
|
|
196
|
+
screenReaderText={t("selectAll")}
|
|
140
197
|
select={
|
|
141
198
|
!isRadio
|
|
142
199
|
? {
|
|
143
|
-
onSelect: (_, isSelected
|
|
144
|
-
onSelect!(isSelected, rowIndex);
|
|
200
|
+
onSelect: (_, isSelected) => {
|
|
145
201
|
updateState(-1, isSelected);
|
|
146
202
|
},
|
|
147
|
-
isSelected:
|
|
148
|
-
selectedRows.filter((r) => r === true).length ===
|
|
149
|
-
rows.length,
|
|
203
|
+
isSelected: rowsSelectedOnPage.length === rows.length,
|
|
150
204
|
}
|
|
151
205
|
: undefined
|
|
152
206
|
}
|
|
@@ -154,7 +208,8 @@ function DataTable<T>({
|
|
|
154
208
|
)}
|
|
155
209
|
{columns.map((column) => (
|
|
156
210
|
<Th
|
|
157
|
-
|
|
211
|
+
screenReaderText={t("expandRow")}
|
|
212
|
+
key={column.displayKey || column.name}
|
|
158
213
|
className={column.transforms?.[0]().className}
|
|
159
214
|
>
|
|
160
215
|
{t(column.displayKey || column.name)}
|
|
@@ -166,28 +221,26 @@ function DataTable<T>({
|
|
|
166
221
|
<Tbody>
|
|
167
222
|
{(rows as IRow[]).map((row, index) => (
|
|
168
223
|
<Tr key={index} isExpanded={expandedRows[index]}>
|
|
169
|
-
{
|
|
224
|
+
{canSelect && (
|
|
170
225
|
<Td
|
|
171
226
|
select={{
|
|
172
227
|
rowIndex: index,
|
|
173
228
|
onSelect: (_, isSelected, rowIndex) => {
|
|
174
|
-
onSelect!(isSelected, rowIndex);
|
|
175
229
|
updateState(rowIndex, isSelected);
|
|
176
230
|
},
|
|
177
|
-
isSelected: selectedRows
|
|
231
|
+
isSelected: !!selectedRows.find(
|
|
232
|
+
(v) => get(v, "id") === row.data.id,
|
|
233
|
+
),
|
|
178
234
|
variant: isRadio ? "radio" : "checkbox",
|
|
179
235
|
}}
|
|
180
236
|
/>
|
|
181
237
|
)}
|
|
182
|
-
<CellRenderer
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
/>
|
|
189
|
-
</Td>
|
|
190
|
-
)}
|
|
238
|
+
<CellRenderer
|
|
239
|
+
row={row}
|
|
240
|
+
index={index}
|
|
241
|
+
actions={actions}
|
|
242
|
+
actionResolver={actionResolver}
|
|
243
|
+
/>
|
|
191
244
|
</Tr>
|
|
192
245
|
))}
|
|
193
246
|
</Tbody>
|
|
@@ -209,14 +262,19 @@ function DataTable<T>({
|
|
|
209
262
|
},
|
|
210
263
|
}}
|
|
211
264
|
/>
|
|
212
|
-
<CellRenderer
|
|
265
|
+
<CellRenderer
|
|
266
|
+
row={row}
|
|
267
|
+
index={index}
|
|
268
|
+
actions={actions}
|
|
269
|
+
actionResolver={actionResolver}
|
|
270
|
+
/>
|
|
213
271
|
</Tr>
|
|
214
272
|
) : (
|
|
215
273
|
<Tr isExpanded={!!expandedRows[index - 1]}>
|
|
216
274
|
<Td />
|
|
217
275
|
<Td colSpan={columns.length}>
|
|
218
276
|
<ExpandableRowContent>
|
|
219
|
-
<
|
|
277
|
+
<ExpandableRowRenderer row={row} />
|
|
220
278
|
</ExpandableRowContent>
|
|
221
279
|
</Td>
|
|
222
280
|
</Tr>
|
|
@@ -480,38 +538,6 @@ export function KeycloakDataTable<T>({
|
|
|
480
538
|
return action;
|
|
481
539
|
});
|
|
482
540
|
|
|
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
541
|
const onCollapse = (isOpen: boolean, rowIndex: number) => {
|
|
516
542
|
(data![rowIndex] as Row<T>).isOpen = isOpen;
|
|
517
543
|
setRows([...data!]);
|
|
@@ -561,7 +587,12 @@ export function KeycloakDataTable<T>({
|
|
|
561
587
|
<DataTable
|
|
562
588
|
{...props}
|
|
563
589
|
canSelectAll={canSelectAll}
|
|
564
|
-
|
|
590
|
+
canSelect={!!onSelect}
|
|
591
|
+
selected={selected}
|
|
592
|
+
onSelect={(selected) => {
|
|
593
|
+
setSelected(selected);
|
|
594
|
+
onSelect?.(selected);
|
|
595
|
+
}}
|
|
565
596
|
onCollapse={detailColumns ? onCollapse : undefined}
|
|
566
597
|
actions={convertAction()}
|
|
567
598
|
actionResolver={actionResolver}
|
|
@@ -36,19 +36,14 @@ 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
|
|
|
@@ -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>
|
|
@@ -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
|
+
attribute.name,
|
|
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))}
|
|
@@ -94,34 +94,9 @@ export function setUserProfileServerError<T>(
|
|
|
94
94
|
|
|
95
95
|
export function isRequiredAttribute({
|
|
96
96
|
required,
|
|
97
|
-
validators,
|
|
98
97
|
}: 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;
|
|
98
|
+
// Check if required is true
|
|
99
|
+
return required as boolean;
|
|
125
100
|
}
|
|
126
101
|
|
|
127
102
|
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": "260103.0.1",
|
|
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.10",
|
|
13
|
+
"@patternfly/react-icons": "^5.4.2",
|
|
14
|
+
"@patternfly/react-styles": "^5.4.1",
|
|
15
|
+
"@patternfly/react-table": "^5.4.13",
|
|
16
|
+
"i18next": "^24.2.1",
|
|
17
17
|
"lodash-es": "^4.17.21",
|
|
18
18
|
"react": "^18.3.1",
|
|
19
|
-
"react-hook-form": "7.
|
|
20
|
-
"react-i18next": "^15.0
|
|
21
|
-
"@keycloak/keycloak-admin-client": "26.
|
|
22
|
-
"keycloak-js": "26.
|
|
19
|
+
"react-hook-form": "7.54.2",
|
|
20
|
+
"react-i18next": "^15.4.0",
|
|
21
|
+
"@keycloak/keycloak-admin-client": "26.1.3",
|
|
22
|
+
"keycloak-js": "26.1.3",
|
|
23
23
|
"@types/lodash-es": "^4.17.12",
|
|
24
|
-
"@types/react": "^18.3.
|
|
24
|
+
"@types/react": "^18.3.13"
|
|
25
25
|
},
|
|
26
26
|
"publishConfig": {
|
|
27
27
|
"access": "public"
|