@keycloakify/keycloak-ui-shared 26.0.6001
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/LICENSE +2 -0
- package/README.md +6 -0
- package/keycloak-theme/shared/keycloak-ui-shared/alerts/AlertPanel.tsx +43 -0
- package/keycloak-theme/shared/keycloak-ui-shared/alerts/Alerts.tsx +82 -0
- package/keycloak-theme/shared/keycloak-ui-shared/buttons/FormSubmitButton.tsx +47 -0
- package/keycloak-theme/shared/keycloak-ui-shared/context/ErrorPage.tsx +60 -0
- package/keycloak-theme/shared/keycloak-ui-shared/context/HelpContext.tsx +30 -0
- package/keycloak-theme/shared/keycloak-ui-shared/context/KeycloakContext.tsx +97 -0
- package/keycloak-theme/shared/keycloak-ui-shared/context/environment.ts +50 -0
- package/keycloak-theme/shared/keycloak-ui-shared/continue-cancel/ContinueCancelModal.tsx +75 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/FormErrorText.tsx +23 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/FormLabel.tsx +40 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/HelpItem.tsx +43 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/KeycloakSpinner.tsx +12 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/NumberControl.tsx +93 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/OrganizationTable.tsx +122 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/PasswordControl.tsx +71 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/PasswordInput.tsx +50 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/SwitchControl.tsx +67 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/TextAreaControl.tsx +60 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/TextControl.tsx +75 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/keycloak-text-area/KeycloakTextArea.tsx +23 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/select-control/SelectControl.tsx +75 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/select-control/SingleSelectControl.tsx +109 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/select-control/TypeaheadSelectControl.tsx +285 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/table/KeycloakDataTable.tsx +597 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/table/ListEmptyState.tsx +86 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/table/PaginatingTableToolbar.tsx +106 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/table/TableToolbar.tsx +92 -0
- package/keycloak-theme/shared/keycloak-ui-shared/icons/IconMapper.tsx +63 -0
- package/keycloak-theme/shared/keycloak-ui-shared/index.ts +1 -0
- package/keycloak-theme/shared/keycloak-ui-shared/main.ts +96 -0
- package/keycloak-theme/shared/keycloak-ui-shared/masthead/DefaultAvatar.tsx +109 -0
- package/keycloak-theme/shared/keycloak-ui-shared/masthead/KeycloakDropdown.tsx +48 -0
- package/keycloak-theme/shared/keycloak-ui-shared/masthead/Masthead.tsx +161 -0
- package/keycloak-theme/shared/keycloak-ui-shared/scroll-form/FormPanel.tsx +29 -0
- package/keycloak-theme/shared/keycloak-ui-shared/scroll-form/FormTitle.tsx +28 -0
- package/keycloak-theme/shared/keycloak-ui-shared/scroll-form/ScrollForm.tsx +98 -0
- package/keycloak-theme/shared/keycloak-ui-shared/scroll-form/ScrollPanel.tsx +21 -0
- package/keycloak-theme/shared/keycloak-ui-shared/scroll-form/form-title.module.css +4 -0
- package/keycloak-theme/shared/keycloak-ui-shared/scroll-form/scroll-form.module.css +8 -0
- package/keycloak-theme/shared/keycloak-ui-shared/select/KeycloakSelect.tsx +49 -0
- package/keycloak-theme/shared/keycloak-ui-shared/select/SingleSelect.tsx +89 -0
- package/keycloak-theme/shared/keycloak-ui-shared/select/TypeaheadSelect.tsx +198 -0
- package/keycloak-theme/shared/keycloak-ui-shared/user-profile/LocaleSelector.tsx +51 -0
- package/keycloak-theme/shared/keycloak-ui-shared/user-profile/MultiInputComponent.tsx +146 -0
- package/keycloak-theme/shared/keycloak-ui-shared/user-profile/OptionsComponent.tsx +63 -0
- package/keycloak-theme/shared/keycloak-ui-shared/user-profile/SelectComponent.tsx +109 -0
- package/keycloak-theme/shared/keycloak-ui-shared/user-profile/TextAreaComponent.tsx +23 -0
- package/keycloak-theme/shared/keycloak-ui-shared/user-profile/TextComponent.tsx +32 -0
- package/keycloak-theme/shared/keycloak-ui-shared/user-profile/UserProfileFields.tsx +243 -0
- package/keycloak-theme/shared/keycloak-ui-shared/user-profile/UserProfileGroup.tsx +71 -0
- package/keycloak-theme/shared/keycloak-ui-shared/user-profile/utils.ts +170 -0
- package/keycloak-theme/shared/keycloak-ui-shared/utils/ErrorBoundary.tsx +77 -0
- package/keycloak-theme/shared/keycloak-ui-shared/utils/createNamedContext.ts +11 -0
- package/keycloak-theme/shared/keycloak-ui-shared/utils/darkMode.ts +19 -0
- package/keycloak-theme/shared/keycloak-ui-shared/utils/errors.ts +55 -0
- package/keycloak-theme/shared/keycloak-ui-shared/utils/generateId.ts +1 -0
- package/keycloak-theme/shared/keycloak-ui-shared/utils/getRuleValue.ts +17 -0
- package/keycloak-theme/shared/keycloak-ui-shared/utils/isDefined.ts +3 -0
- package/keycloak-theme/shared/keycloak-ui-shared/utils/useFetch.ts +44 -0
- package/keycloak-theme/shared/keycloak-ui-shared/utils/useRequiredContext.ts +24 -0
- package/keycloak-theme/shared/keycloak-ui-shared/utils/useSetTimeout.ts +40 -0
- package/keycloak-theme/shared/keycloak-ui-shared/utils/useStorageItem.ts +51 -0
- package/keycloak-theme/shared/keycloak-ui-shared/utils/useStoredState.ts +38 -0
- package/package.json +31 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { UserProfileAttributeMetadata } from "@keycloak/keycloak-admin-client/lib/defs/userProfileMetadata";
|
|
2
|
+
import UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation";
|
|
3
|
+
import { TFunction } from "i18next";
|
|
4
|
+
import { FieldPath } from "react-hook-form";
|
|
5
|
+
|
|
6
|
+
export type KeyValueType = { key: string; value: string };
|
|
7
|
+
|
|
8
|
+
export type UserFormFields = Omit<
|
|
9
|
+
UserRepresentation,
|
|
10
|
+
"attributes" | "userProfileMetadata"
|
|
11
|
+
> & {
|
|
12
|
+
attributes?: KeyValueType[] | Record<string, string | string[]>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type FieldError = {
|
|
16
|
+
field: string;
|
|
17
|
+
errorMessage: string;
|
|
18
|
+
params?: string[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type ErrorArray = { errors?: FieldError[] };
|
|
22
|
+
|
|
23
|
+
export type UserProfileError = {
|
|
24
|
+
responseData: ErrorArray | FieldError;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const isBundleKey = (displayName?: string) => displayName?.includes("${");
|
|
28
|
+
const unWrap = (key: string) => key.substring(2, key.length - 1);
|
|
29
|
+
|
|
30
|
+
export const label = (
|
|
31
|
+
t: TFunction,
|
|
32
|
+
text: string | undefined,
|
|
33
|
+
fallback?: string,
|
|
34
|
+
prefix?: string,
|
|
35
|
+
) => {
|
|
36
|
+
const value = text || fallback;
|
|
37
|
+
const bundleKey = isBundleKey(value) ? unWrap(value!) : value;
|
|
38
|
+
const key = prefix ? `${prefix}.${bundleKey}` : bundleKey;
|
|
39
|
+
return t(key || "");
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const labelAttribute = (
|
|
43
|
+
t: TFunction,
|
|
44
|
+
attribute: UserProfileAttributeMetadata,
|
|
45
|
+
) => label(t, attribute.displayName, attribute.name);
|
|
46
|
+
|
|
47
|
+
const ROOT_ATTRIBUTES = ["username", "firstName", "lastName", "email"];
|
|
48
|
+
|
|
49
|
+
export const isRootAttribute = (attr?: string) =>
|
|
50
|
+
attr && ROOT_ATTRIBUTES.includes(attr);
|
|
51
|
+
|
|
52
|
+
export const fieldName = (name?: string) =>
|
|
53
|
+
`${isRootAttribute(name) ? "" : "attributes."}${name?.replaceAll(
|
|
54
|
+
".",
|
|
55
|
+
"🍺",
|
|
56
|
+
)}` as FieldPath<UserFormFields>;
|
|
57
|
+
|
|
58
|
+
export const beerify = <T extends string>(name: T) =>
|
|
59
|
+
name.replaceAll(".", "🍺");
|
|
60
|
+
|
|
61
|
+
export const debeerify = <T extends string>(name: T) =>
|
|
62
|
+
name.replaceAll("🍺", ".");
|
|
63
|
+
|
|
64
|
+
export function setUserProfileServerError<T>(
|
|
65
|
+
error: UserProfileError,
|
|
66
|
+
setError: (field: keyof T, params: object) => void,
|
|
67
|
+
t: TFunction,
|
|
68
|
+
) {
|
|
69
|
+
(
|
|
70
|
+
((error.responseData as ErrorArray).errors !== undefined
|
|
71
|
+
? (error.responseData as ErrorArray).errors
|
|
72
|
+
: [error.responseData]) as FieldError[]
|
|
73
|
+
).forEach((e) => {
|
|
74
|
+
const params = Object.assign(
|
|
75
|
+
{},
|
|
76
|
+
e.params?.map((p) => (isBundleKey(p.toString()) ? t(unWrap(p)) : p)),
|
|
77
|
+
);
|
|
78
|
+
setError(fieldName(e.field) as keyof T, {
|
|
79
|
+
message: t(
|
|
80
|
+
isBundleKey(e.errorMessage) ? unWrap(e.errorMessage) : e.errorMessage,
|
|
81
|
+
{
|
|
82
|
+
...params,
|
|
83
|
+
defaultValue: e.errorMessage || e.field,
|
|
84
|
+
},
|
|
85
|
+
),
|
|
86
|
+
type: "server",
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function isRequiredAttribute({
|
|
92
|
+
required,
|
|
93
|
+
validators,
|
|
94
|
+
}: UserProfileAttributeMetadata): boolean {
|
|
95
|
+
// Check if required is true or if the validators include a validation that would make the attribute implicitly required.
|
|
96
|
+
return required || hasRequiredValidators(validators);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Checks whether the given validators include a validation that would make the attribute implicitly required.
|
|
101
|
+
*/
|
|
102
|
+
function hasRequiredValidators(
|
|
103
|
+
validators?: UserProfileAttributeMetadata["validators"],
|
|
104
|
+
): boolean {
|
|
105
|
+
// If we don't have any validators, the attribute is not required.
|
|
106
|
+
if (!validators) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// If the 'length' validator is defined and has a minimal length greater than zero the attribute is implicitly required.
|
|
111
|
+
// We have to do a lot of defensive coding here, because we don't have type information for the validators.
|
|
112
|
+
if (
|
|
113
|
+
"length" in validators &&
|
|
114
|
+
"min" in validators.length &&
|
|
115
|
+
typeof validators.length.min === "number"
|
|
116
|
+
) {
|
|
117
|
+
return validators.length.min > 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function isUserProfileError(error: unknown): error is UserProfileError {
|
|
124
|
+
// Check if the error is an object with a 'responseData' property.
|
|
125
|
+
if (
|
|
126
|
+
typeof error !== "object" ||
|
|
127
|
+
error === null ||
|
|
128
|
+
!("responseData" in error)
|
|
129
|
+
) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const { responseData } = error;
|
|
134
|
+
|
|
135
|
+
if (isFieldError(responseData)) {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Check if 'responseData' is an object with an 'errors' property that is an array.
|
|
140
|
+
if (
|
|
141
|
+
typeof responseData !== "object" ||
|
|
142
|
+
responseData === null ||
|
|
143
|
+
!("errors" in responseData) ||
|
|
144
|
+
!Array.isArray(responseData.errors)
|
|
145
|
+
) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Check if all errors are field errors.
|
|
150
|
+
return responseData.errors.every(isFieldError);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function isFieldError(error: unknown): error is FieldError {
|
|
154
|
+
// Check if the error is an object.
|
|
155
|
+
if (typeof error !== "object" || error === null) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Check if the error object has a 'field' property that is a string.
|
|
160
|
+
if (!("field" in error) || typeof error.field !== "string") {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Check if the error object has an 'errorMessage' property that is a string.
|
|
165
|
+
if (!("errorMessage" in error) || typeof error.errorMessage !== "string") {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
type ComponentType,
|
|
4
|
+
type FunctionComponent,
|
|
5
|
+
type GetDerivedStateFromError,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
} from "react";
|
|
8
|
+
import { createNamedContext } from "./createNamedContext";
|
|
9
|
+
import { useRequiredContext } from "./useRequiredContext";
|
|
10
|
+
|
|
11
|
+
export interface ErrorBoundaryContextValue {
|
|
12
|
+
error?: Error;
|
|
13
|
+
showBoundary: (error?: Error) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const ErrorBoundaryContext = createNamedContext<
|
|
17
|
+
ErrorBoundaryContextValue | undefined
|
|
18
|
+
>("ErrorBoundaryContext", undefined);
|
|
19
|
+
|
|
20
|
+
export const useErrorBoundary = () => useRequiredContext(ErrorBoundaryContext);
|
|
21
|
+
|
|
22
|
+
export interface ErrorBoundaryProviderProps {
|
|
23
|
+
children: ReactNode;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ErrorBoundaryProviderState {
|
|
27
|
+
error?: Error;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class ErrorBoundaryProvider extends Component<
|
|
31
|
+
ErrorBoundaryProviderProps,
|
|
32
|
+
ErrorBoundaryProviderState
|
|
33
|
+
> {
|
|
34
|
+
state: ErrorBoundaryProviderState = {};
|
|
35
|
+
|
|
36
|
+
static getDerivedStateFromError: GetDerivedStateFromError<
|
|
37
|
+
ErrorBoundaryProviderProps,
|
|
38
|
+
ErrorBoundaryProviderState
|
|
39
|
+
> = (error) => {
|
|
40
|
+
return { error };
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
showBoundary = (error?: Error) => {
|
|
44
|
+
this.setState({ error });
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
render() {
|
|
48
|
+
return (
|
|
49
|
+
<ErrorBoundaryContext.Provider
|
|
50
|
+
value={{ error: this.state.error, showBoundary: this.showBoundary }}
|
|
51
|
+
>
|
|
52
|
+
{this.props.children}
|
|
53
|
+
</ErrorBoundaryContext.Provider>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface FallbackProps {
|
|
59
|
+
error: Error;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface ErrorBoundaryFallbackProps {
|
|
63
|
+
fallback: ComponentType<FallbackProps>;
|
|
64
|
+
children: ReactNode;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const ErrorBoundaryFallback: FunctionComponent<
|
|
68
|
+
ErrorBoundaryFallbackProps
|
|
69
|
+
> = ({ children, fallback: FallbackComponent }) => {
|
|
70
|
+
const { error } = useErrorBoundary();
|
|
71
|
+
|
|
72
|
+
if (error) {
|
|
73
|
+
return <FallbackComponent error={error} />;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return children;
|
|
77
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Context } from "react";
|
|
2
|
+
import { createContext } from "react";
|
|
3
|
+
|
|
4
|
+
export type NamedContext<T> = Context<T> &
|
|
5
|
+
Required<Pick<Context<T>, "displayName">>;
|
|
6
|
+
|
|
7
|
+
export function createNamedContext<T>(displayName: string, defaultValue: T) {
|
|
8
|
+
const context = createContext(defaultValue);
|
|
9
|
+
context.displayName = displayName;
|
|
10
|
+
return context as NamedContext<T>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const DARK_MODE_CLASS = "pf-v5-theme-dark";
|
|
2
|
+
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
|
3
|
+
|
|
4
|
+
function updateDarkMode(isEnabled: boolean) {
|
|
5
|
+
const { classList } = document.documentElement;
|
|
6
|
+
|
|
7
|
+
if (isEnabled) {
|
|
8
|
+
classList.add(DARK_MODE_CLASS);
|
|
9
|
+
} else {
|
|
10
|
+
classList.remove(DARK_MODE_CLASS);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function initializeDarkMode() {
|
|
15
|
+
updateDarkMode(mediaQuery.matches);
|
|
16
|
+
mediaQuery.addEventListener("change", (event) =>
|
|
17
|
+
updateDarkMode(event.matches),
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { NetworkError } from "@keycloak/keycloak-admin-client";
|
|
2
|
+
|
|
3
|
+
const ERROR_FIELDS = ["error", "errorMessage"];
|
|
4
|
+
const ERROR_DESCRIPTION_FIELD = "error_description";
|
|
5
|
+
|
|
6
|
+
export function getErrorMessage(error: unknown) {
|
|
7
|
+
if (typeof error === "string") {
|
|
8
|
+
return error;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (error instanceof NetworkError) {
|
|
12
|
+
return getNetworkErrorMessage(error.responseData);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (error instanceof Error) {
|
|
16
|
+
return error.message;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
throw new Error("Unable to determine error message.");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getErrorDescription(error: unknown) {
|
|
23
|
+
if (!(error instanceof NetworkError)) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const data = error.responseData;
|
|
28
|
+
|
|
29
|
+
return getNetworkErrorDescription(data);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getNetworkErrorDescription(data: unknown) {
|
|
33
|
+
if (
|
|
34
|
+
typeof data === "object" &&
|
|
35
|
+
data !== null &&
|
|
36
|
+
ERROR_DESCRIPTION_FIELD in data &&
|
|
37
|
+
typeof data[ERROR_DESCRIPTION_FIELD] === "string"
|
|
38
|
+
) {
|
|
39
|
+
return data[ERROR_DESCRIPTION_FIELD];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getNetworkErrorMessage(data: unknown) {
|
|
44
|
+
if (typeof data !== "object" || data === null) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const key of ERROR_FIELDS) {
|
|
49
|
+
const value = (data as Record<string, unknown>)[key];
|
|
50
|
+
|
|
51
|
+
if (typeof value === "string") {
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const generateId = () => Math.floor(Math.random() * 1000);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ValidationRule, ValidationValue } from "react-hook-form";
|
|
2
|
+
|
|
3
|
+
// Simplified version of https://github.com/react-hook-form/react-hook-form/blob/ea0f3ed86457691f79987a703ae8d50b9e16e2ad/src/logic/getRuleValue.ts#L10-L21
|
|
4
|
+
// TODO: Can be removed if https://github.com/react-hook-form/react-hook-form/issues/12178 is resolved
|
|
5
|
+
export function getRuleValue<T extends ValidationValue>(
|
|
6
|
+
rule?: ValidationRule<T>,
|
|
7
|
+
): T | undefined {
|
|
8
|
+
if (typeof rule === "undefined" || rule instanceof RegExp) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (typeof rule === "object") {
|
|
13
|
+
return rule.value;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return rule;
|
|
17
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { DependencyList, useEffect } from "react";
|
|
2
|
+
import { useErrorBoundary } from "./ErrorBoundary";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Util function to only set the state when the component is still mounted.
|
|
6
|
+
*
|
|
7
|
+
* It takes 2 functions one you do your adminClient call in and the other to set your state
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* useFetch(
|
|
11
|
+
* () => adminClient.components.findOne({ id }),
|
|
12
|
+
* (component) => setupForm(component),
|
|
13
|
+
* []
|
|
14
|
+
* );
|
|
15
|
+
*
|
|
16
|
+
* @param adminClientCall use this to do your adminClient call
|
|
17
|
+
* @param callback when the data is fetched this is where you set your state
|
|
18
|
+
*/
|
|
19
|
+
export function useFetch<T>(
|
|
20
|
+
adminClientCall: () => Promise<T>,
|
|
21
|
+
callback: (param: T) => void,
|
|
22
|
+
deps?: DependencyList,
|
|
23
|
+
) {
|
|
24
|
+
const { showBoundary } = useErrorBoundary();
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const controller = new AbortController();
|
|
28
|
+
const { signal } = controller;
|
|
29
|
+
adminClientCall()
|
|
30
|
+
.then((result) => {
|
|
31
|
+
if (!signal.aborted) {
|
|
32
|
+
callback(result);
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
.catch((error) => {
|
|
36
|
+
console.error(error);
|
|
37
|
+
if (!signal.aborted) {
|
|
38
|
+
showBoundary(error);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return () => controller.abort();
|
|
43
|
+
}, deps);
|
|
44
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Context } from "react";
|
|
2
|
+
import { useContext } from "react";
|
|
3
|
+
import { isDefined } from "./isDefined";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Passes the call to `useContext` and throw an exception if the resolved value is either `null` or `undefined`.
|
|
7
|
+
* Can be used for contexts that are required and should always have a non nullable value.
|
|
8
|
+
*
|
|
9
|
+
* @param context The context to pass to `useContext`
|
|
10
|
+
* @returns
|
|
11
|
+
*/
|
|
12
|
+
export function useRequiredContext<T>(context: Context<T>): NonNullable<T> {
|
|
13
|
+
const resolved = useContext(context);
|
|
14
|
+
|
|
15
|
+
if (isDefined(resolved)) {
|
|
16
|
+
return resolved;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
throw new Error(
|
|
20
|
+
`No provider found for ${
|
|
21
|
+
context.displayName ? `the '${context.displayName}'` : "an unknown"
|
|
22
|
+
} context, make sure it is included in your component hierarchy.`,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useEffect, useRef, useCallback } from "react";
|
|
2
|
+
|
|
3
|
+
export function useSetTimeout() {
|
|
4
|
+
const didUnmountRef = useRef(false);
|
|
5
|
+
const scheduledTimersRef = useRef(new Set<number>());
|
|
6
|
+
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
didUnmountRef.current = false;
|
|
9
|
+
|
|
10
|
+
return () => {
|
|
11
|
+
didUnmountRef.current = true;
|
|
12
|
+
clearAll();
|
|
13
|
+
};
|
|
14
|
+
}, []);
|
|
15
|
+
|
|
16
|
+
function clearAll() {
|
|
17
|
+
scheduledTimersRef.current.forEach((timer) => clearTimeout(timer));
|
|
18
|
+
scheduledTimersRef.current.clear();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return useCallback((callback: () => void, delay: number) => {
|
|
22
|
+
if (didUnmountRef.current) {
|
|
23
|
+
throw new Error("Can't schedule a timeout on an unmounted component.");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const timer = Number(setTimeout(handleCallback, delay));
|
|
27
|
+
|
|
28
|
+
scheduledTimersRef.current.add(timer);
|
|
29
|
+
|
|
30
|
+
function handleCallback() {
|
|
31
|
+
scheduledTimersRef.current.delete(timer);
|
|
32
|
+
callback();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return function cancelTimeout() {
|
|
36
|
+
clearTimeout(timer);
|
|
37
|
+
scheduledTimersRef.current.delete(timer);
|
|
38
|
+
};
|
|
39
|
+
}, []);
|
|
40
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Dispatch, useCallback, useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A hook that allows you to get a specific item stored by the [Web Storage API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API).
|
|
5
|
+
* Automatically updates the value when modified in the context of another document (such as an open tab) trough the [`storage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event) event.
|
|
6
|
+
*
|
|
7
|
+
* @param storageArea The storage area to target, must implement the [`Storage`](https://developer.mozilla.org/en-US/docs/Web/API/Storage) interface (such as [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) and [`sessionStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage)).
|
|
8
|
+
* @param keyName The key of the item to get from storage, same as passed to [`Storage.getItem()`](https://developer.mozilla.org/en-US/docs/Web/API/Storage/getItem)
|
|
9
|
+
* @param The default value to fall back to in case no stored value was retrieved.
|
|
10
|
+
*/
|
|
11
|
+
export function useStorageItem(
|
|
12
|
+
storageArea: Storage,
|
|
13
|
+
keyName: string,
|
|
14
|
+
defaultValue: string,
|
|
15
|
+
): [string, Dispatch<string>] {
|
|
16
|
+
const [value, setInnerValue] = useState(
|
|
17
|
+
() => storageArea.getItem(keyName) ?? defaultValue,
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const setValue = useCallback((newValue: string) => {
|
|
21
|
+
setInnerValue(newValue);
|
|
22
|
+
storageArea.setItem(keyName, newValue);
|
|
23
|
+
}, []);
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
// If the key name or storage area has changed, we want to update the value.
|
|
27
|
+
// React will only set state if it actually changed, so no need to worry about re-renders.
|
|
28
|
+
setInnerValue(storageArea.getItem(keyName) ?? defaultValue);
|
|
29
|
+
|
|
30
|
+
// Subscribe to storage events so we can update the value when it is changed within the context of another document.
|
|
31
|
+
window.addEventListener("storage", handleStorage);
|
|
32
|
+
|
|
33
|
+
function handleStorage(event: StorageEvent) {
|
|
34
|
+
// If the affected storage area is different we can ignore this event.
|
|
35
|
+
// For example, if we're using session storage we're not interested in changes from local storage.
|
|
36
|
+
if (event.storageArea !== storageArea) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// If the event key is null then it means all storage was cleared.
|
|
41
|
+
// Therefore we're interested in keys that are, or that match the key name.
|
|
42
|
+
if (event.key === null || event.key === keyName) {
|
|
43
|
+
setInnerValue(event.newValue ?? defaultValue);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return () => window.removeEventListener("storage", handleStorage);
|
|
48
|
+
}, [storageArea, keyName]);
|
|
49
|
+
|
|
50
|
+
return [value, setValue];
|
|
51
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Dispatch, useCallback, useMemo } from "react";
|
|
2
|
+
import { useStorageItem } from "./useStorageItem";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A hook that acts similarly to React's `useState()`, but persists the state using [Web Storage API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API).
|
|
6
|
+
* Automatically updates the value when modified in the context of another document (such as an open tab) trough the [`storage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event) event.
|
|
7
|
+
*
|
|
8
|
+
* The value is serialized as [JSON](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON) and therefore the value provided must be serializable as such.
|
|
9
|
+
* Because the value is always serialized it will never be referentially equal to originally provided value.
|
|
10
|
+
*
|
|
11
|
+
* @param storageArea The storage area to target, must implement the [`Storage`](https://developer.mozilla.org/en-US/docs/Web/API/Storage) interface (such as [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) and [`sessionStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage)).
|
|
12
|
+
* @param keyName The key of the item to get from storage, same as passed to [`Storage.getItem()`](https://developer.mozilla.org/en-US/docs/Web/API/Storage/getItem)
|
|
13
|
+
* @param defaultValue The default value to fall back to in case no stored value was retrieved (must be serializable as JSON).
|
|
14
|
+
*/
|
|
15
|
+
export function useStoredState<S>(
|
|
16
|
+
storageArea: Storage,
|
|
17
|
+
keyName: string,
|
|
18
|
+
defaultValue: S,
|
|
19
|
+
): [S, Dispatch<S>] {
|
|
20
|
+
const defaultValueSerialized = useMemo(
|
|
21
|
+
() => JSON.stringify(defaultValue),
|
|
22
|
+
[defaultValue],
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const [storedValue, setStoredValue] = useStorageItem(
|
|
26
|
+
storageArea,
|
|
27
|
+
keyName,
|
|
28
|
+
defaultValueSerialized,
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const value = useMemo<S>(() => JSON.parse(storedValue), [storedValue]);
|
|
32
|
+
const setValue = useCallback(
|
|
33
|
+
(value: S) => setStoredValue(JSON.stringify(value)),
|
|
34
|
+
[],
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
return [value, setValue];
|
|
38
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@keycloakify/keycloak-ui-shared",
|
|
3
|
+
"version": "26.0.6001",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "git://github.com/keycloakify/keycloak-ui-shared.git"
|
|
7
|
+
},
|
|
8
|
+
"license": "Apache-2.0",
|
|
9
|
+
"author": "The Keycloak Team, re-packaged by u/garronej",
|
|
10
|
+
"homepage": "https://github.com/keycloakify/keycloak-ui-shared",
|
|
11
|
+
"peerDependencies": {
|
|
12
|
+
"@patternfly/react-core": "^5.4.1",
|
|
13
|
+
"@patternfly/react-icons": "^5.4.0",
|
|
14
|
+
"@patternfly/react-styles": "^5.4.0",
|
|
15
|
+
"@patternfly/react-table": "^5.4.1",
|
|
16
|
+
"i18next": "^23.15.1",
|
|
17
|
+
"lodash-es": "^4.17.21",
|
|
18
|
+
"react": "^18.3.1",
|
|
19
|
+
"react-dom": "^18.3.1",
|
|
20
|
+
"react-hook-form": "7.53.0",
|
|
21
|
+
"react-i18next": "^15.0.2",
|
|
22
|
+
"keycloak-js": "26.0.6",
|
|
23
|
+
"@keycloak/keycloak-admin-client": "26.0.6",
|
|
24
|
+
"@types/lodash-es": "^4.17.12",
|
|
25
|
+
"@types/react": "^18.3.11",
|
|
26
|
+
"@types/react-dom": "^18.3.0"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
}
|
|
31
|
+
}
|