@datalayer/core 1.2.14 → 1.2.17
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/lib/components/anonymous/AnonymousKeyExpired.d.ts +7 -1
- package/lib/components/anonymous/AnonymousKeyExpired.js +2 -2
- package/lib/components/labels/StatusLabels.d.ts +11 -0
- package/lib/components/labels/StatusLabels.js +15 -4
- package/lib/hooks/index.d.ts +1 -0
- package/lib/hooks/index.js +1 -0
- package/lib/hooks/libraryApp.d.ts +37 -0
- package/lib/hooks/libraryApp.js +59 -0
- package/lib/hooks/useCache.js +8 -1
- package/lib/models/UserSettings.d.ts +11 -0
- package/lib/models/UserSettings.js +9 -0
- package/lib/views/iam/SignInSimple.d.ts +6 -0
- package/lib/views/iam/SignInSimple.js +6 -2
- package/package.json +1 -1
|
@@ -51,6 +51,12 @@ export type AnonymousKeyExpiredProps = {
|
|
|
51
51
|
temporary?: boolean;
|
|
52
52
|
/** Called after a successful sign-in, once the token is stored. */
|
|
53
53
|
onSignedIn?: () => void;
|
|
54
|
+
/**
|
|
55
|
+
* Called on *Sign up with email*, for a host with a router to send the
|
|
56
|
+
* reader to its own sign-up page; without it, the form opens its sign-up
|
|
57
|
+
* address.
|
|
58
|
+
*/
|
|
59
|
+
onSignUp?: () => void;
|
|
54
60
|
};
|
|
55
|
-
export declare function AnonymousKeyExpired({ agentName, sandboxStillRuns, temporary, onSignedIn, }: AnonymousKeyExpiredProps): JSX.Element;
|
|
61
|
+
export declare function AnonymousKeyExpired({ agentName, sandboxStillRuns, temporary, onSignedIn, onSignUp, }: AnonymousKeyExpiredProps): JSX.Element;
|
|
56
62
|
export default AnonymousKeyExpired;
|
|
@@ -5,7 +5,7 @@ import { Box } from '@datalayer/primer-addons';
|
|
|
5
5
|
import { KeyIcon } from '@primer/octicons-react';
|
|
6
6
|
import { SignInSimple } from '../../views/iam/SignInSimple.js';
|
|
7
7
|
import { useCoreStore, iamStore } from '../../state/substates/index.js';
|
|
8
|
-
export function AnonymousKeyExpired({ agentName, sandboxStillRuns = false, temporary = true, onSignedIn, }) {
|
|
8
|
+
export function AnonymousKeyExpired({ agentName, sandboxStillRuns = false, temporary = true, onSignedIn, onSignUp, }) {
|
|
9
9
|
const { configuration } = useCoreStore();
|
|
10
10
|
const handleSignIn = useCallback((token) => {
|
|
11
11
|
/*
|
|
@@ -91,6 +91,6 @@ export function AnonymousKeyExpired({ agentName, sandboxStillRuns = false, tempo
|
|
|
91
91
|
// has one and harmless for one that does not.
|
|
92
92
|
loginUrl: configuration?.iamUrl
|
|
93
93
|
? `${configuration.iamUrl}/api/iam/v1/login`
|
|
94
|
-
: undefined, onSignIn: handleSignIn })] }) }));
|
|
94
|
+
: undefined, onSignIn: handleSignIn, onSignUp: onSignUp })] }) }));
|
|
95
95
|
}
|
|
96
96
|
export default AnonymousKeyExpired;
|
|
@@ -18,6 +18,17 @@ export type StatusLabelProps = {
|
|
|
18
18
|
size?: 'small' | 'large';
|
|
19
19
|
sx?: Record<string, unknown>;
|
|
20
20
|
};
|
|
21
|
+
/**
|
|
22
|
+
* The tone of each named label. A role or a stage is a quality, not a
|
|
23
|
+
* verdict, so none is in attention's amber: the theme keeps success, attention
|
|
24
|
+
* and danger for verdicts (LOOP T-14, T-29) — *admin* is quiet.
|
|
25
|
+
*/
|
|
26
|
+
export declare const NAMED_LABEL_TONES: {
|
|
27
|
+
readonly alpha: "accent";
|
|
28
|
+
readonly admin: "neutral";
|
|
29
|
+
readonly default: "accent";
|
|
30
|
+
readonly soon: "neutral";
|
|
31
|
+
};
|
|
21
32
|
/**
|
|
22
33
|
* The pill the named labels below are made of.
|
|
23
34
|
*
|
|
@@ -22,6 +22,17 @@ const TONES = {
|
|
|
22
22
|
borderColor: 'border.default',
|
|
23
23
|
},
|
|
24
24
|
};
|
|
25
|
+
/**
|
|
26
|
+
* The tone of each named label. A role or a stage is a quality, not a
|
|
27
|
+
* verdict, so none is in attention's amber: the theme keeps success, attention
|
|
28
|
+
* and danger for verdicts (LOOP T-14, T-29) — *admin* is quiet.
|
|
29
|
+
*/
|
|
30
|
+
export const NAMED_LABEL_TONES = {
|
|
31
|
+
alpha: 'accent',
|
|
32
|
+
admin: 'neutral',
|
|
33
|
+
default: 'accent',
|
|
34
|
+
soon: 'neutral',
|
|
35
|
+
};
|
|
25
36
|
/**
|
|
26
37
|
* The pill the named labels below are made of.
|
|
27
38
|
*
|
|
@@ -42,25 +53,25 @@ export function StatusLabel(props) {
|
|
|
42
53
|
*/
|
|
43
54
|
export function AlphaLabel(props = {}) {
|
|
44
55
|
const { children = 'alpha', ...rest } = props;
|
|
45
|
-
return (_jsx(StatusLabel, { tone:
|
|
56
|
+
return (_jsx(StatusLabel, { tone: NAMED_LABEL_TONES.alpha, ...rest, children: children }));
|
|
46
57
|
}
|
|
47
58
|
/**
|
|
48
59
|
* Something reserved to the administrators of the platform.
|
|
49
60
|
*/
|
|
50
61
|
export function AdminLabel(props = {}) {
|
|
51
62
|
const { children = 'admin', ...rest } = props;
|
|
52
|
-
return (_jsx(StatusLabel, { tone:
|
|
63
|
+
return (_jsx(StatusLabel, { tone: NAMED_LABEL_TONES.admin, ...rest, children: children }));
|
|
53
64
|
}
|
|
54
65
|
/**
|
|
55
66
|
* The one that is taken when none is chosen — the default space of an account.
|
|
56
67
|
*/
|
|
57
68
|
export function DefaultLabel(props = {}) {
|
|
58
69
|
const { children = 'default', ...rest } = props;
|
|
59
|
-
return (_jsx(StatusLabel, { tone:
|
|
70
|
+
return (_jsx(StatusLabel, { tone: NAMED_LABEL_TONES.default, ...rest, children: children }));
|
|
60
71
|
}
|
|
61
72
|
/** "soon": what is announced but not yet there. */
|
|
62
73
|
export function SoonLabel(props = {}) {
|
|
63
74
|
const { children = 'soon', ...rest } = props;
|
|
64
|
-
return (_jsx(StatusLabel, { tone:
|
|
75
|
+
return (_jsx(StatusLabel, { tone: NAMED_LABEL_TONES.soon, ...rest, children: children }));
|
|
65
76
|
}
|
|
66
77
|
export default StatusLabel;
|
package/lib/hooks/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export * from './useAuthorization';
|
|
|
2
2
|
export * from './useBackdrop';
|
|
3
3
|
export * from './useBackdropJupyterLab';
|
|
4
4
|
export * from './useCache';
|
|
5
|
+
export * from './libraryApp';
|
|
5
6
|
export * from './useContainsFocus';
|
|
6
7
|
export * from './useContents';
|
|
7
8
|
export * from './useDatalayer';
|
package/lib/hooks/index.js
CHANGED
|
@@ -6,6 +6,7 @@ export * from './useAuthorization.js';
|
|
|
6
6
|
export * from './useBackdrop.js';
|
|
7
7
|
export * from './useBackdropJupyterLab.js';
|
|
8
8
|
export * from './useCache.js';
|
|
9
|
+
export * from './libraryApp.js';
|
|
9
10
|
export * from './useContainsFocus.js';
|
|
10
11
|
export * from './useContents.js';
|
|
11
12
|
export * from './useDatalayer.js';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the Library says of a published application beyond any item's fields:
|
|
3
|
+
* its face, its kind and whether it runs live.
|
|
4
|
+
*
|
|
5
|
+
* Spacer keeps them beside the app's item, from its Appspec, and the Library
|
|
6
|
+
* projection carries them: `face_s` (the JSON of its `id`, `emoji`, `avatar`
|
|
7
|
+
* and `banner`, each only when set), `app_kind_s` and `runs_live_b` — an app
|
|
8
|
+
* that runs live can be tried before it is cloned: not a decision, and naming
|
|
9
|
+
* the agent it runs on.
|
|
10
|
+
*
|
|
11
|
+
* @module hooks/libraryApp
|
|
12
|
+
*/
|
|
13
|
+
/** The kinds of application. */
|
|
14
|
+
export declare const LIBRARY_APP_KINDS: readonly ["decision", "chat", "widget", "worker"];
|
|
15
|
+
export type LibraryAppKind = (typeof LIBRARY_APP_KINDS)[number];
|
|
16
|
+
/** An application's face, as its Appspec says it: each only when set. */
|
|
17
|
+
export type LibraryAppFace = {
|
|
18
|
+
/** The Appspec's id: what seeds its banner, and the agent it runs as. */
|
|
19
|
+
id?: string;
|
|
20
|
+
emoji?: string;
|
|
21
|
+
/** The drawing chosen, by name, from the profile's set. */
|
|
22
|
+
avatar?: string;
|
|
23
|
+
/** The banner chosen, by name, from the profile's set. */
|
|
24
|
+
banner?: string;
|
|
25
|
+
};
|
|
26
|
+
export type LibraryAppFields = {
|
|
27
|
+
/** None when the Library answered without one. */
|
|
28
|
+
face?: LibraryAppFace;
|
|
29
|
+
/** None when the Library answered without one. */
|
|
30
|
+
appKind?: LibraryAppKind;
|
|
31
|
+
/** Whether it can be tried before it is cloned. */
|
|
32
|
+
runsLive: boolean;
|
|
33
|
+
};
|
|
34
|
+
/** The face `face_s` carries; a value that is not a face's JSON is refused. */
|
|
35
|
+
export declare function libraryAppFaceOf(value: unknown): LibraryAppFace | undefined;
|
|
36
|
+
/** The face, kind and liveness of an app as the Library answers it. */
|
|
37
|
+
export declare function libraryAppFieldsOf(raw: Record<string, unknown>): LibraryAppFields;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2023-2025 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* What the Library says of a published application beyond any item's fields:
|
|
7
|
+
* its face, its kind and whether it runs live.
|
|
8
|
+
*
|
|
9
|
+
* Spacer keeps them beside the app's item, from its Appspec, and the Library
|
|
10
|
+
* projection carries them: `face_s` (the JSON of its `id`, `emoji`, `avatar`
|
|
11
|
+
* and `banner`, each only when set), `app_kind_s` and `runs_live_b` — an app
|
|
12
|
+
* that runs live can be tried before it is cloned: not a decision, and naming
|
|
13
|
+
* the agent it runs on.
|
|
14
|
+
*
|
|
15
|
+
* @module hooks/libraryApp
|
|
16
|
+
*/
|
|
17
|
+
/** The kinds of application. */
|
|
18
|
+
export const LIBRARY_APP_KINDS = [
|
|
19
|
+
'decision',
|
|
20
|
+
'chat',
|
|
21
|
+
'widget',
|
|
22
|
+
'worker',
|
|
23
|
+
];
|
|
24
|
+
const FACE_KEYS = ['id', 'emoji', 'avatar', 'banner'];
|
|
25
|
+
/** The face `face_s` carries; a value that is not a face's JSON is refused. */
|
|
26
|
+
export function libraryAppFaceOf(value) {
|
|
27
|
+
if (value === undefined || value === null || value === '') {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
if (typeof value !== 'string') {
|
|
31
|
+
throw new Error(`An app's face is not a string: ${String(value)}`);
|
|
32
|
+
}
|
|
33
|
+
const parsed = JSON.parse(value);
|
|
34
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
35
|
+
throw new Error(`An app's face is not an object: ${value}`);
|
|
36
|
+
}
|
|
37
|
+
const face = {};
|
|
38
|
+
for (const key of FACE_KEYS) {
|
|
39
|
+
const field = parsed[key];
|
|
40
|
+
if (typeof field === 'string' && field) {
|
|
41
|
+
face[key] = field;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return face;
|
|
45
|
+
}
|
|
46
|
+
/** The face, kind and liveness of an app as the Library answers it. */
|
|
47
|
+
export function libraryAppFieldsOf(raw) {
|
|
48
|
+
const kind = raw.app_kind_s;
|
|
49
|
+
if (kind !== undefined &&
|
|
50
|
+
!LIBRARY_APP_KINDS.includes(kind)) {
|
|
51
|
+
throw new Error(`An app's kind is not one of ${LIBRARY_APP_KINDS}: ${String(kind)}`);
|
|
52
|
+
}
|
|
53
|
+
const face = libraryAppFaceOf(raw.face_s);
|
|
54
|
+
return {
|
|
55
|
+
...(face ? { face } : {}),
|
|
56
|
+
...(kind ? { appKind: kind } : {}),
|
|
57
|
+
runsLive: raw.runs_live_b === true,
|
|
58
|
+
};
|
|
59
|
+
}
|
package/lib/hooks/useCache.js
CHANGED
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
import { useQuery, useQueries, useMutation, useQueryClient, } from '@tanstack/react-query';
|
|
40
40
|
import { BOOTSTRAP_USER_ONBOARDING, asContact, asInvite, asOrganization, asSecret, asTeam, asToken, asUser, } from '../models/index.js';
|
|
41
41
|
import { asSpace } from './cacheConverters.js';
|
|
42
|
+
import { libraryAppFieldsOf } from './libraryApp.js';
|
|
42
43
|
import { useCoreStore, useIAMStore, profileStore } from '../state/index.js';
|
|
43
44
|
import { asDisplayName, namesAsInitials, asArray } from '../utils/index.js';
|
|
44
45
|
import { useDatalayer } from './useDatalayer.js';
|
|
@@ -880,10 +881,13 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
|
|
|
880
881
|
/**
|
|
881
882
|
* A published agentic UI application: what it answers, and its definition
|
|
882
883
|
* as the library hands it over — `model_s`, the string the Studio reads
|
|
883
|
-
* the definition from, left unparsed because the Studio owns its shape
|
|
884
|
+
* the definition from, left unparsed because the Studio owns its shape —
|
|
885
|
+
* and its face, its kind and whether it runs live (`libraryAppFieldsOf`),
|
|
886
|
+
* so its card draws it and the gallery opens one that runs live.
|
|
884
887
|
*/
|
|
885
888
|
const toApp = (raw) => {
|
|
886
889
|
return {
|
|
890
|
+
...libraryAppFieldsOf(raw),
|
|
887
891
|
id: raw.uid,
|
|
888
892
|
type: 'app',
|
|
889
893
|
name: raw.name_t,
|
|
@@ -1836,6 +1840,9 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
|
|
|
1836
1840
|
if (settings.docsInPlace !== undefined) {
|
|
1837
1841
|
body.docs_in_place_b = settings.docsInPlace;
|
|
1838
1842
|
}
|
|
1843
|
+
if (settings.showAdvanced !== undefined) {
|
|
1844
|
+
body.show_advanced_b = settings.showAdvanced;
|
|
1845
|
+
}
|
|
1839
1846
|
return requestDatalayer({
|
|
1840
1847
|
url: `${configuration.iamUrl}/api/iam/v1/users/${userId}/settings`,
|
|
1841
1848
|
method: 'PUT',
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export declare const AI_AGENTS_URL_KEY = "aiAgents_url_s";
|
|
2
2
|
export declare const CAN_INVITE_URL_KEY = "can_invite_b";
|
|
3
3
|
export declare const DOCS_IN_PLACE_KEY = "docs_in_place_b";
|
|
4
|
+
export declare const SHOW_ADVANCED_KEY = "show_advanced_b";
|
|
4
5
|
export type IUserSettings = {
|
|
5
6
|
aiAgentsUrl?: string;
|
|
6
7
|
canInvite?: boolean;
|
|
@@ -13,11 +14,19 @@ export type IUserSettings = {
|
|
|
13
14
|
* full-window documentation.
|
|
14
15
|
*/
|
|
15
16
|
docsInPlace?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Whether the Studio shows what a Maker does not need — *Show advanced*.
|
|
19
|
+
*
|
|
20
|
+
* Undefined means the user has never chosen: the Studio makes a first
|
|
21
|
+
* choice for them, and keeps it here.
|
|
22
|
+
*/
|
|
23
|
+
showAdvanced?: boolean;
|
|
16
24
|
};
|
|
17
25
|
export declare class UserSettings implements IUserSettings {
|
|
18
26
|
private _aiAgentsUrl?;
|
|
19
27
|
private _canInvite?;
|
|
20
28
|
private _docsInPlace?;
|
|
29
|
+
private _showAdvanced?;
|
|
21
30
|
constructor(s: any);
|
|
22
31
|
get aiAgentsUrl(): string | undefined;
|
|
23
32
|
set aiAgentsUrl(value: string | undefined);
|
|
@@ -25,5 +34,7 @@ export declare class UserSettings implements IUserSettings {
|
|
|
25
34
|
set canInvite(value: boolean | undefined);
|
|
26
35
|
get docsInPlace(): boolean | undefined;
|
|
27
36
|
set docsInPlace(value: boolean | undefined);
|
|
37
|
+
get showAdvanced(): boolean | undefined;
|
|
38
|
+
set showAdvanced(value: boolean | undefined);
|
|
28
39
|
}
|
|
29
40
|
export default UserSettings;
|
|
@@ -5,14 +5,17 @@
|
|
|
5
5
|
export const AI_AGENTS_URL_KEY = 'aiAgents_url_s';
|
|
6
6
|
export const CAN_INVITE_URL_KEY = 'can_invite_b';
|
|
7
7
|
export const DOCS_IN_PLACE_KEY = 'docs_in_place_b';
|
|
8
|
+
export const SHOW_ADVANCED_KEY = 'show_advanced_b';
|
|
8
9
|
export class UserSettings {
|
|
9
10
|
_aiAgentsUrl;
|
|
10
11
|
_canInvite;
|
|
11
12
|
_docsInPlace;
|
|
13
|
+
_showAdvanced;
|
|
12
14
|
constructor(s) {
|
|
13
15
|
this._aiAgentsUrl = s[AI_AGENTS_URL_KEY] ?? undefined;
|
|
14
16
|
this._canInvite = s[CAN_INVITE_URL_KEY] ?? undefined;
|
|
15
17
|
this._docsInPlace = s[DOCS_IN_PLACE_KEY] ?? undefined;
|
|
18
|
+
this._showAdvanced = s[SHOW_ADVANCED_KEY] ?? undefined;
|
|
16
19
|
}
|
|
17
20
|
get aiAgentsUrl() {
|
|
18
21
|
return this._aiAgentsUrl;
|
|
@@ -32,5 +35,11 @@ export class UserSettings {
|
|
|
32
35
|
set docsInPlace(value) {
|
|
33
36
|
this._docsInPlace = value;
|
|
34
37
|
}
|
|
38
|
+
get showAdvanced() {
|
|
39
|
+
return this._showAdvanced;
|
|
40
|
+
}
|
|
41
|
+
set showAdvanced(value) {
|
|
42
|
+
this._showAdvanced = value;
|
|
43
|
+
}
|
|
35
44
|
}
|
|
36
45
|
export default UserSettings;
|
|
@@ -122,6 +122,12 @@ export interface SignInSimpleProps {
|
|
|
122
122
|
* Sign-up URL opened when clicking the sign-up button.
|
|
123
123
|
*/
|
|
124
124
|
signUpHref?: string;
|
|
125
|
+
/**
|
|
126
|
+
* Called on the sign-up button instead of opening `signUpHref`: a host
|
|
127
|
+
* with a router sends the reader to its own sign-up page without leaving
|
|
128
|
+
* the app.
|
|
129
|
+
*/
|
|
130
|
+
onSignUp?: () => void;
|
|
125
131
|
/**
|
|
126
132
|
* Forgot-password URL for password form.
|
|
127
133
|
*/
|
|
@@ -101,7 +101,7 @@ const SignInInputGlobalStyle = createGlobalStyle `
|
|
|
101
101
|
}
|
|
102
102
|
`;
|
|
103
103
|
// ── Component ────────────────────────────────────────────────────────
|
|
104
|
-
export const SignInSimple = ({ onSignIn, onApiKeySignIn, loginUrl: loginUrlProp, name, title = 'Datalayer OTEL', description = 'Sign in to access the observability dashboard.', icon, leadingIcon = _jsx(TelescopeIcon, { size: 24 }), github = false, google = false, linkedin = false, apiKey = false, signUp = true, socialSignInNavigationTarget, asDoc = false, fillHeight = true, hideHero = false, calloutTitle, calloutDescription, passwordToggleLabel = 'Sign in with a password', signUpTitle = "Don't have an account?", signUpDescription = 'Create a free Datalayer account with your email address.', signUpLabel = 'Sign up with email', signUpHref = 'https://datalayer.ai/signup', forgotPasswordHref = '/password', forgotPasswordLabel = 'Forgot password?', showForgotPassword = true, socialButtonMarginTop = 3, actionButtonMaxWidth = 320, }) => {
|
|
104
|
+
export const SignInSimple = ({ onSignIn, onApiKeySignIn, loginUrl: loginUrlProp, name, title = 'Datalayer OTEL', description = 'Sign in to access the observability dashboard.', icon, leadingIcon = _jsx(TelescopeIcon, { size: 24 }), github = false, google = false, linkedin = false, apiKey = false, signUp = true, socialSignInNavigationTarget, asDoc = false, fillHeight = true, hideHero = false, calloutTitle, calloutDescription, passwordToggleLabel = 'Sign in with a password', signUpTitle = "Don't have an account?", signUpDescription = 'Create a free Datalayer account with your email address.', signUpLabel = 'Sign up with email', signUpHref = 'https://datalayer.ai/signup', onSignUp, forgotPasswordHref = '/password', forgotPasswordLabel = 'Forgot password?', showForgotPassword = true, socialButtonMarginTop = 3, actionButtonMaxWidth = 320, }) => {
|
|
105
105
|
const compactDocMode = asDoc && !hideHero;
|
|
106
106
|
const headingText = name ?? (asDoc && title === 'Datalayer OTEL' ? 'Datalayer Sign In' : title);
|
|
107
107
|
const headingIcon = icon ?? (asDoc ? _jsx(SignInIcon, { size: 24 }) : leadingIcon);
|
|
@@ -208,8 +208,12 @@ export const SignInSimple = ({ onSignIn, onApiKeySignIn, loginUrl: loginUrlProp,
|
|
|
208
208
|
closeApiKeyDialog();
|
|
209
209
|
}, [apiKeyValue, onApiKeySignIn, closeApiKeyDialog]);
|
|
210
210
|
const handleSignUp = useCallback(() => {
|
|
211
|
+
if (onSignUp) {
|
|
212
|
+
onSignUp();
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
211
215
|
window.location.assign(signUpHref);
|
|
212
|
-
}, [signUpHref]);
|
|
216
|
+
}, [onSignUp, signUpHref]);
|
|
213
217
|
const handleForgotPassword = useCallback(() => {
|
|
214
218
|
if (!forgotPasswordHref) {
|
|
215
219
|
return;
|