@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.
Files changed (22) hide show
  1. package/keycloak-theme/shared/keycloak-ui-shared/context/ErrorPage.tsx +7 -10
  2. package/keycloak-theme/shared/keycloak-ui-shared/context/KeycloakContext.tsx +3 -9
  3. package/keycloak-theme/shared/keycloak-ui-shared/controls/FileUploadControl.tsx +85 -0
  4. package/keycloak-theme/shared/keycloak-ui-shared/controls/FormLabel.tsx +4 -2
  5. package/keycloak-theme/shared/keycloak-ui-shared/controls/OrganizationTable.tsx +3 -0
  6. package/keycloak-theme/shared/keycloak-ui-shared/controls/SwitchControl.tsx +3 -2
  7. package/keycloak-theme/shared/keycloak-ui-shared/controls/TextAreaControl.tsx +1 -0
  8. package/keycloak-theme/shared/keycloak-ui-shared/controls/select-control/SelectControl.tsx +5 -0
  9. package/keycloak-theme/shared/keycloak-ui-shared/controls/select-control/SingleSelectControl.tsx +16 -5
  10. package/keycloak-theme/shared/keycloak-ui-shared/controls/select-control/TypeaheadSelectControl.tsx +65 -35
  11. package/keycloak-theme/shared/keycloak-ui-shared/controls/table/KeycloakDataTable.tsx +56 -44
  12. package/keycloak-theme/shared/keycloak-ui-shared/controls/table/ListEmptyState.tsx +9 -2
  13. package/keycloak-theme/shared/keycloak-ui-shared/controls/table/TableToolbar.tsx +1 -1
  14. package/keycloak-theme/shared/keycloak-ui-shared/main.ts +4 -0
  15. package/keycloak-theme/shared/keycloak-ui-shared/masthead/Masthead.tsx +3 -0
  16. package/keycloak-theme/shared/keycloak-ui-shared/scroll-form/ScrollForm.tsx +9 -2
  17. package/keycloak-theme/shared/keycloak-ui-shared/select/KeycloakSelect.tsx +1 -1
  18. package/keycloak-theme/shared/keycloak-ui-shared/select/TypeaheadSelect.tsx +5 -1
  19. package/keycloak-theme/shared/keycloak-ui-shared/user-profile/LocaleSelector.tsx +1 -1
  20. package/keycloak-theme/shared/keycloak-ui-shared/user-profile/TextComponent.tsx +2 -2
  21. package/keycloak-theme/shared/keycloak-ui-shared/user-profile/utils.ts +10 -3
  22. 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 { getNetworkErrorDescription } from "../utils/errors";
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
- getErrorMessage(error) ||
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={errorMessage ? "" : t("somethingWentWrong")}
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: "check-sso",
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
- 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
- );
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<T, P> & {
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) => {
@@ -57,6 +57,7 @@ export const TextAreaControl = <
57
57
  fieldState.error ? ValidatedOptions.error : ValidatedOptions.default
58
58
  }
59
59
  isDisabled={props.isDisabled}
60
+ {...props}
60
61
  {...field}
61
62
  />
62
63
  </FormLabel>
@@ -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 = (
@@ -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.slice(name.lastIndexOf(".") + 1)}
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="toggle"
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
- onChange(Array.isArray(value) ? [option] : option);
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>
@@ -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 filteredOptions = options.filter((option) =>
69
- getValue(option).toLowerCase().startsWith(filterValue.toLowerCase()),
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 convert = useMemo(
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.onChange(
104
- Array.isArray(field.value)
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(options)
171
- ? options
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.slice(name.lastIndexOf(".") + 1)}
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(options)
200
- ? options.find(
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(options)
239
- ? options.find((o) => selection === o.key)
240
- ?.value
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
- if (field.value.includes(option)) {
271
- field.onChange(
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>{convert}</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
- {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
- );
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 (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
- );
163
+ if (isRadio) {
164
+ const selectedRow = isSelected ? [rows[rowIndex].data] : [];
165
+ updateSelectedRows(selectedRow);
172
166
  } else {
173
- if (isSelected) {
174
- updateSelectedRows([...selectedRows, rows[rowIndex].data]);
175
- } else {
167
+ if (rowIndex === -1) {
168
+ const rowsSelectedOnPageIds = rowsSelectedOnPage.map((v) =>
169
+ get(v, "id"),
170
+ );
176
171
  updateSelectedRows(
177
- selectedRows.filter(
178
- (v) => get(v, "id") !== (rows[rowIndex] as IRow).data.id,
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
- isExpanded: !!expandedRows[index],
255
- rowIndex: index,
256
- expandId: `${index}`,
257
- onToggle: (_, rowIndex, isOpen) => {
258
- onCollapse(isOpen, rowIndex);
259
- const expand = [...expandedRows];
260
- expand[index] = isOpen;
261
- setExpandedRows(expand);
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
- {(loading || !noData || searching) && (
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 { ComponentClass, MouseEventHandler, ReactNode } from "react";
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
- }: ListEmptyStateProps) => {
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) => (
@@ -49,7 +49,7 @@ export const TableToolbar = ({
49
49
 
50
50
  return (
51
51
  <>
52
- <Toolbar>
52
+ <Toolbar data-testid="table-toolbar">
53
53
  <ToolbarContent>
54
54
  {inputGroupName && (
55
55
  <ToolbarItem>
@@ -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
- href={`#${scrollId}`}
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) => 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
- attribute.name,
30
+ "",
31
31
  attribute.annotations?.[
32
32
  "inputOptionLabelsI18nPrefix"
33
33
  ] as string,
34
34
  )
35
35
  }
36
- readOnly={attribute.readOnly}
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?: string[];
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?: string) => displayName?.includes("${");
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) => (isBundleKey(p.toString()) ? t(unWrap(p)) : 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": "260103.0.1",
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.10",
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.13",
16
- "i18next": "^24.2.1",
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.54.2",
20
- "react-i18next": "^15.4.0",
21
- "@keycloak/keycloak-admin-client": "26.1.3",
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.13"
24
+ "@types/react": "^18.3.18"
25
25
  },
26
26
  "publishConfig": {
27
27
  "access": "public"