@flamingo-stack/openframe-frontend-core 0.0.585 → 0.0.586-snapshot.20260903151444
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/dist/{chunk-XK4S47LG.cjs → chunk-3FHWLQDG.cjs} +10 -10
- package/dist/{chunk-XK4S47LG.cjs.map → chunk-3FHWLQDG.cjs.map} +1 -1
- package/dist/{chunk-CTZP2NJQ.js → chunk-3PDTGDOA.js} +4 -4
- package/dist/{chunk-UG6SHUGE.js → chunk-436G4DI4.js} +2 -2
- package/dist/{chunk-ULGI56BW.cjs → chunk-4IIFSFJ3.cjs} +13 -13
- package/dist/{chunk-ULGI56BW.cjs.map → chunk-4IIFSFJ3.cjs.map} +1 -1
- package/dist/{chunk-4TN6C7ZG.cjs → chunk-5O5JMGEN.cjs} +33 -33
- package/dist/{chunk-4TN6C7ZG.cjs.map → chunk-5O5JMGEN.cjs.map} +1 -1
- package/dist/{chunk-HQRKC6P7.js → chunk-6BFEJXMI.js} +2 -2
- package/dist/{chunk-F5SV627B.cjs → chunk-7WIKOL25.cjs} +4 -4
- package/dist/{chunk-F5SV627B.cjs.map → chunk-7WIKOL25.cjs.map} +1 -1
- package/dist/{chunk-VQ5FMCZA.cjs → chunk-A5PXHMCJ.cjs} +49 -49
- package/dist/{chunk-VQ5FMCZA.cjs.map → chunk-A5PXHMCJ.cjs.map} +1 -1
- package/dist/{chunk-36GXR7BR.js → chunk-BLJEPNMJ.js} +3 -3
- package/dist/{chunk-QIVZG4XK.cjs → chunk-DMQMRFGS.cjs} +16 -16
- package/dist/{chunk-QIVZG4XK.cjs.map → chunk-DMQMRFGS.cjs.map} +1 -1
- package/dist/{chunk-HBI7AWUE.cjs → chunk-DTQCZ6AR.cjs} +17 -17
- package/dist/{chunk-HBI7AWUE.cjs.map → chunk-DTQCZ6AR.cjs.map} +1 -1
- package/dist/{chunk-6VFO4JUH.cjs → chunk-E7NFHRLX.cjs} +3 -3
- package/dist/{chunk-6VFO4JUH.cjs.map → chunk-E7NFHRLX.cjs.map} +1 -1
- package/dist/{chunk-276BXVOB.js → chunk-F3YRPKUA.js} +2 -2
- package/dist/{chunk-WXL2AR5I.js → chunk-HCWZP44O.js} +2026 -1912
- package/dist/chunk-HCWZP44O.js.map +1 -0
- package/dist/{chunk-FQJBV5ON.cjs → chunk-IGBNEPTD.cjs} +3 -3
- package/dist/{chunk-FQJBV5ON.cjs.map → chunk-IGBNEPTD.cjs.map} +1 -1
- package/dist/{chunk-DZM7KTHE.cjs → chunk-KQ5JQVYL.cjs} +34 -34
- package/dist/{chunk-DZM7KTHE.cjs.map → chunk-KQ5JQVYL.cjs.map} +1 -1
- package/dist/{chunk-JKWWACNI.js → chunk-LODCKZ72.js} +2 -2
- package/dist/{chunk-BU2KLFRB.cjs → chunk-MF7TMFVB.cjs} +3 -3
- package/dist/{chunk-BU2KLFRB.cjs.map → chunk-MF7TMFVB.cjs.map} +1 -1
- package/dist/{chunk-HAIRDHWL.cjs → chunk-MXCWOMNT.cjs} +3 -3
- package/dist/{chunk-HAIRDHWL.cjs.map → chunk-MXCWOMNT.cjs.map} +1 -1
- package/dist/{chunk-SCMESLXP.js → chunk-OKD7MX7Y.js} +4 -4
- package/dist/{chunk-LNKBNY3A.js → chunk-P24OI2CD.js} +2 -2
- package/dist/{chunk-GG76BPXJ.js → chunk-R2U2D7LU.js} +2 -2
- package/dist/{chunk-J4MVZ53H.js → chunk-SI57FQZ2.js} +2 -2
- package/dist/{chunk-7554QJ6R.js → chunk-SVR75PFW.js} +5 -5
- package/dist/{chunk-3F7V7GFU.cjs → chunk-UBSIENYF.cjs} +408 -294
- package/dist/chunk-UBSIENYF.cjs.map +1 -0
- package/dist/{chunk-FCKOWGTE.js → chunk-V4WUSOGS.js} +2 -2
- package/dist/{chunk-VSXFBO4D.js → chunk-VWNU2PFX.js} +2 -2
- package/dist/{chunk-27DK5GCC.cjs → chunk-WQXWH7LF.cjs} +4 -4
- package/dist/{chunk-27DK5GCC.cjs.map → chunk-WQXWH7LF.cjs.map} +1 -1
- package/dist/{chunk-5YVCIBUH.cjs → chunk-XNEPABUD.cjs} +17 -17
- package/dist/{chunk-5YVCIBUH.cjs.map → chunk-XNEPABUD.cjs.map} +1 -1
- package/dist/{chunk-BNZFLIEG.js → chunk-YB5DFQKD.js} +5 -5
- package/dist/components/case-studies/index.cjs +8 -8
- package/dist/components/case-studies/index.js +2 -2
- package/dist/components/contact/index.cjs +3 -3
- package/dist/components/contact/index.js +2 -2
- package/dist/components/docs/index.cjs +6 -6
- package/dist/components/docs/index.js +5 -5
- package/dist/components/embeds/index.cjs +3 -3
- package/dist/components/embeds/index.js +2 -2
- package/dist/components/faq/index.cjs +3 -3
- package/dist/components/faq/index.js +2 -2
- package/dist/components/features/auth/account-details-fields.d.ts +34 -0
- package/dist/components/features/auth/account-details-fields.d.ts.map +1 -0
- package/dist/components/features/auth/complete-account-form.d.ts.map +1 -1
- package/dist/components/features/auth/create-organization-form.d.ts +43 -6
- package/dist/components/features/auth/create-organization-form.d.ts.map +1 -1
- package/dist/components/features/auth/index.d.ts +2 -0
- package/dist/components/features/auth/index.d.ts.map +1 -1
- package/dist/components/features/auth/labeled-divider.d.ts +6 -0
- package/dist/components/features/auth/labeled-divider.d.ts.map +1 -0
- package/dist/components/features/auth/login-form.d.ts +32 -9
- package/dist/components/features/auth/login-form.d.ts.map +1 -1
- package/dist/components/features/auth/openframe-sso-signup-form.d.ts.map +1 -1
- package/dist/components/features/auth/sso-providers.d.ts +1 -1
- package/dist/components/features/auth/sso-providers.d.ts.map +1 -1
- package/dist/components/features/index.cjs +6 -2
- package/dist/components/features/index.cjs.map +1 -1
- package/dist/components/features/index.js +5 -1
- package/dist/components/help-center-pages/index.cjs +22 -22
- package/dist/components/help-center-pages/index.js +13 -13
- package/dist/components/index.cjs +33 -29
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +16 -12
- package/dist/components/index.js.map +1 -1
- package/dist/components/meeting-scheduler/index.cjs +18 -18
- package/dist/components/meeting-scheduler/index.js +1 -1
- package/dist/components/navigation/index.cjs +5 -5
- package/dist/components/navigation/index.js +4 -4
- package/dist/components/onboarding-guides/index.cjs +5 -5
- package/dist/components/onboarding-guides/index.js +4 -4
- package/dist/components/tickets/index.cjs +6 -6
- package/dist/components/tickets/index.js +5 -5
- package/dist/components/ui/data-table/use-data-table.d.ts.map +1 -1
- package/dist/components/ui/index.cjs +2 -2
- package/dist/components/ui/index.js +1 -1
- package/dist/index.cjs +10 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +9 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/features/auth/account-details-fields.tsx +124 -0
- package/src/components/features/auth/complete-account-form.tsx +13 -52
- package/src/components/features/auth/create-organization-form.tsx +128 -38
- package/src/components/features/auth/index.ts +2 -0
- package/src/components/features/auth/labeled-divider.tsx +16 -0
- package/src/components/features/auth/login-form.tsx +102 -23
- package/src/components/features/auth/openframe-sso-signup-form.tsx +11 -43
- package/src/components/features/auth/sso-providers.tsx +3 -3
- package/src/components/ui/__tests__/data-table-compiler-identity.test.tsx +74 -0
- package/src/components/ui/data-table/use-data-table.ts +36 -1
- package/src/stories/auth/Login.stories.tsx +15 -5
- package/dist/chunk-3F7V7GFU.cjs.map +0 -1
- package/dist/chunk-WXL2AR5I.js.map +0 -1
- /package/dist/{chunk-CTZP2NJQ.js.map → chunk-3PDTGDOA.js.map} +0 -0
- /package/dist/{chunk-UG6SHUGE.js.map → chunk-436G4DI4.js.map} +0 -0
- /package/dist/{chunk-HQRKC6P7.js.map → chunk-6BFEJXMI.js.map} +0 -0
- /package/dist/{chunk-36GXR7BR.js.map → chunk-BLJEPNMJ.js.map} +0 -0
- /package/dist/{chunk-276BXVOB.js.map → chunk-F3YRPKUA.js.map} +0 -0
- /package/dist/{chunk-JKWWACNI.js.map → chunk-LODCKZ72.js.map} +0 -0
- /package/dist/{chunk-SCMESLXP.js.map → chunk-OKD7MX7Y.js.map} +0 -0
- /package/dist/{chunk-LNKBNY3A.js.map → chunk-P24OI2CD.js.map} +0 -0
- /package/dist/{chunk-GG76BPXJ.js.map → chunk-R2U2D7LU.js.map} +0 -0
- /package/dist/{chunk-J4MVZ53H.js.map → chunk-SI57FQZ2.js.map} +0 -0
- /package/dist/{chunk-7554QJ6R.js.map → chunk-SVR75PFW.js.map} +0 -0
- /package/dist/{chunk-FCKOWGTE.js.map → chunk-V4WUSOGS.js.map} +0 -0
- /package/dist/{chunk-VSXFBO4D.js.map → chunk-VWNU2PFX.js.map} +0 -0
- /package/dist/{chunk-BNZFLIEG.js.map → chunk-YB5DFQKD.js.map} +0 -0
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import type { KeyboardEvent } from 'react';
|
|
3
|
+
import type { KeyboardEvent, ReactNode } from 'react';
|
|
4
4
|
import { useDeferredError } from '../../../hooks/ui/use-deferred-error';
|
|
5
5
|
import { cn } from '../../../utils/cn';
|
|
6
|
+
import { AlertTriangleIcon } from '../../icons-v2-generated/interface/alert-triangle-icon';
|
|
7
|
+
import { Button } from '../../ui/button';
|
|
6
8
|
import { Input } from '../../ui/input';
|
|
9
|
+
import { LabeledDivider } from './labeled-divider';
|
|
7
10
|
import type { AuthSsoProvider } from './sso-providers';
|
|
8
11
|
import { SsoProviderButtons } from './sso-providers';
|
|
9
12
|
|
|
@@ -11,8 +14,6 @@ export interface LoginFormProps {
|
|
|
11
14
|
/** Controlled email value */
|
|
12
15
|
email: string;
|
|
13
16
|
onEmailChange: (value: string) => void;
|
|
14
|
-
/** Enter-key handler on the email field (optional). */
|
|
15
|
-
onSubmit?: () => void;
|
|
16
17
|
emailPlaceholder?: string;
|
|
17
18
|
loading?: boolean;
|
|
18
19
|
disabled?: boolean;
|
|
@@ -22,8 +23,8 @@ export interface LoginFormProps {
|
|
|
22
23
|
/** Informational status under the email field (e.g. live tenant discovery). `errors.email` wins. */
|
|
23
24
|
emailStatus?: { message: string; variant: 'error' | 'warning' | 'success' | 'muted' };
|
|
24
25
|
/**
|
|
25
|
-
* SSO providers rendered
|
|
26
|
-
* clickability with `ssoDisabled` / `ssoEnabledProviders`.
|
|
26
|
+
* SSO providers, rendered ABOVE the email field with a divider between the two. Always visible;
|
|
27
|
+
* gate clickability with `ssoDisabled` / `ssoEnabledProviders`.
|
|
27
28
|
*/
|
|
28
29
|
ssoProviders: AuthSsoProvider[];
|
|
29
30
|
onSsoClick?: (provider: AuthSsoProvider) => void;
|
|
@@ -31,21 +32,44 @@ export interface LoginFormProps {
|
|
|
31
32
|
ssoDisabled?: boolean;
|
|
32
33
|
/** When set, only these providers are clickable; the rest stay disabled. */
|
|
33
34
|
ssoEnabledProviders?: AuthSsoProvider[];
|
|
34
|
-
/** Verb prefix for provider buttons, e.g. "Continue with".
|
|
35
|
+
/** Verb prefix for provider buttons, e.g. "Continue with". */
|
|
35
36
|
ssoActionLabel?: string;
|
|
37
|
+
/** Label on the divider between the provider buttons and the email field. */
|
|
38
|
+
dividerLabel?: string;
|
|
39
|
+
/**
|
|
40
|
+
* The discovered tenant's own SSO options, rendered UNDER the email field — unlike `ssoProviders`
|
|
41
|
+
* above, these depend on knowing the tenant, so they cannot be offered before an address is
|
|
42
|
+
* typed. Shares `onSsoClick`.
|
|
43
|
+
*
|
|
44
|
+
* Three states, and the empty array is not the same as nothing: `undefined` means discovery has
|
|
45
|
+
* not resolved a tenant yet and the slot stays empty, while `[]` means it resolved and the tenant
|
|
46
|
+
* has none — which is an answer worth showing rather than silence.
|
|
47
|
+
*/
|
|
48
|
+
customSsoProviders?: AuthSsoProvider[];
|
|
49
|
+
/** Shown in place of the custom SSO buttons when the resolved tenant has none configured. */
|
|
50
|
+
noCustomSsoLabel?: string;
|
|
51
|
+
/** Primary submit under the email field. Omit the handler to hide the button. */
|
|
52
|
+
onSubmitClick?: () => void;
|
|
53
|
+
submitLabel?: string;
|
|
54
|
+
/** Disables just the primary submit (fields stay editable). */
|
|
55
|
+
submitDisabled?: boolean;
|
|
56
|
+
/** Overrides the default heading. */
|
|
57
|
+
title?: string;
|
|
58
|
+
/** Overrides the default sub-heading. */
|
|
59
|
+
subtitle?: ReactNode;
|
|
36
60
|
className?: string;
|
|
37
61
|
}
|
|
38
62
|
|
|
39
63
|
/**
|
|
40
64
|
* Login form (Login tab). Presentational + controlled — the consumer owns
|
|
41
|
-
* state, validation and discovery. Single-screen design: the
|
|
42
|
-
* the
|
|
43
|
-
* consumer
|
|
65
|
+
* state, validation and discovery. Single-screen design: the provider buttons come first and need
|
|
66
|
+
* nothing typed, then the email field below them. Whether the providers are gated is the
|
|
67
|
+
* consumer's call via `ssoDisabled`; the identity-first flows leave them open, because the server
|
|
68
|
+
* resolves the tenant from the identity the provider asserts.
|
|
44
69
|
*/
|
|
45
70
|
export function LoginForm({
|
|
46
71
|
email,
|
|
47
72
|
onEmailChange,
|
|
48
|
-
onSubmit,
|
|
49
73
|
emailPlaceholder = 'username@mail.com',
|
|
50
74
|
loading = false,
|
|
51
75
|
disabled = false,
|
|
@@ -56,6 +80,14 @@ export function LoginForm({
|
|
|
56
80
|
ssoDisabled = false,
|
|
57
81
|
ssoEnabledProviders,
|
|
58
82
|
ssoActionLabel = 'Continue with',
|
|
83
|
+
dividerLabel = 'or continue with OpenFrame SSO',
|
|
84
|
+
customSsoProviders,
|
|
85
|
+
noCustomSsoLabel = 'No additional SSO configurations for this domain',
|
|
86
|
+
onSubmitClick,
|
|
87
|
+
submitLabel = 'Continue',
|
|
88
|
+
submitDisabled = false,
|
|
89
|
+
title = 'Login to OpenFrame',
|
|
90
|
+
subtitle = 'Enter your email to access your organization.',
|
|
59
91
|
className,
|
|
60
92
|
}: LoginFormProps) {
|
|
61
93
|
const fieldDisabled = disabled || loading;
|
|
@@ -63,10 +95,11 @@ export function LoginForm({
|
|
|
63
95
|
// Validation messages are deferred while the user is typing (shown on blur or after a pause).
|
|
64
96
|
const emailErr = useDeferredError(errors?.email, email);
|
|
65
97
|
|
|
98
|
+
// Enter drives the same action as the visible button, under the same gate. A form with no
|
|
99
|
+
// submit (the Login tab, where the providers are the only way on) simply does nothing.
|
|
66
100
|
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
|
67
|
-
if (event.key
|
|
68
|
-
|
|
69
|
-
}
|
|
101
|
+
if (event.key !== 'Enter' || fieldDisabled || submitDisabled) return;
|
|
102
|
+
onSubmitClick?.();
|
|
70
103
|
};
|
|
71
104
|
|
|
72
105
|
return (
|
|
@@ -78,10 +111,25 @@ export function LoginForm({
|
|
|
78
111
|
>
|
|
79
112
|
{/* Header */}
|
|
80
113
|
<div className="flex flex-col">
|
|
81
|
-
<h1 className="tracking-[-0.64px] text-ods-text-primary text-h2">
|
|
82
|
-
<p className="text-ods-text-secondary text-h4">
|
|
114
|
+
<h1 className="tracking-[-0.64px] text-ods-text-primary text-h2">{title}</h1>
|
|
115
|
+
<p className="text-ods-text-secondary text-h4">{subtitle}</p>
|
|
83
116
|
</div>
|
|
84
117
|
|
|
118
|
+
{/* Providers come FIRST. They need nothing typed, and putting the email above them taught
|
|
119
|
+
people to fill it in before tapping one — the behaviour that made a provider button look
|
|
120
|
+
broken while it waited on a field it never needed. */}
|
|
121
|
+
{ssoProviders.length > 0 && (
|
|
122
|
+
<SsoProviderButtons
|
|
123
|
+
providers={ssoProviders}
|
|
124
|
+
onSsoClick={onSsoClick}
|
|
125
|
+
actionLabel={ssoActionLabel}
|
|
126
|
+
disabled={disabled || loading || ssoDisabled}
|
|
127
|
+
enabledProviders={ssoEnabledProviders}
|
|
128
|
+
/>
|
|
129
|
+
)}
|
|
130
|
+
|
|
131
|
+
{ssoProviders.length > 0 && <LabeledDivider label={dividerLabel} />}
|
|
132
|
+
|
|
85
133
|
{/* Email */}
|
|
86
134
|
<Input
|
|
87
135
|
type="email"
|
|
@@ -96,14 +144,45 @@ export function LoginForm({
|
|
|
96
144
|
onKeyDown={handleKeyDown}
|
|
97
145
|
/>
|
|
98
146
|
|
|
99
|
-
{
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
147
|
+
{customSsoProviders &&
|
|
148
|
+
(customSsoProviders.length > 0 ? (
|
|
149
|
+
<SsoProviderButtons
|
|
150
|
+
providers={customSsoProviders}
|
|
151
|
+
onSsoClick={onSsoClick}
|
|
152
|
+
actionLabel={ssoActionLabel}
|
|
153
|
+
disabled={disabled || loading}
|
|
154
|
+
/>
|
|
155
|
+
) : (
|
|
156
|
+
// Same footprint as the button it stands in for, so nothing shifts when a tenant does
|
|
157
|
+
// have options. Tokens are the design's: soft-grey surface, grey text and icon, the
|
|
158
|
+
// bold-body step, a 24px icon, and 12/16 for the inset and the gap.
|
|
159
|
+
<div
|
|
160
|
+
role="status"
|
|
161
|
+
className="flex h-11 w-full items-center gap-[var(--spacing-system-m)] rounded-md bg-ods-bg-surface px-[var(--spacing-system-s)] text-ods-text-secondary text-h3 md:h-12"
|
|
162
|
+
>
|
|
163
|
+
<AlertTriangleIcon className="h-6 w-6 shrink-0" />
|
|
164
|
+
{noCustomSsoLabel}
|
|
165
|
+
</div>
|
|
166
|
+
))}
|
|
167
|
+
|
|
168
|
+
{onSubmitClick && (
|
|
169
|
+
// Full width on a phone, half the row and right-aligned from `md` up, matching the design.
|
|
170
|
+
// The spacer, rather than a width on the button, keeps it flush with the fields' right edge.
|
|
171
|
+
<div className="flex items-center gap-[var(--spacing-system-l)]">
|
|
172
|
+
<div className="hidden flex-1 md:block" />
|
|
173
|
+
<Button
|
|
174
|
+
type="button"
|
|
175
|
+
variant="accent"
|
|
176
|
+
fullWidth
|
|
177
|
+
className="md:flex-1"
|
|
178
|
+
loading={loading}
|
|
179
|
+
disabled={submitDisabled || fieldDisabled}
|
|
180
|
+
onClick={onSubmitClick}
|
|
181
|
+
>
|
|
182
|
+
{submitLabel}
|
|
183
|
+
</Button>
|
|
184
|
+
</div>
|
|
185
|
+
)}
|
|
107
186
|
</div>
|
|
108
187
|
);
|
|
109
188
|
}
|
|
@@ -5,7 +5,7 @@ import { useDeferredError } from '../../../hooks/ui/use-deferred-error';
|
|
|
5
5
|
import { cn } from '../../../utils/cn';
|
|
6
6
|
import { Button } from '../../ui/button';
|
|
7
7
|
import { Input } from '../../ui/input';
|
|
8
|
-
import {
|
|
8
|
+
import { AccountDetailsFields } from './account-details-fields';
|
|
9
9
|
|
|
10
10
|
export interface OpenFrameSsoSignUpFormProps {
|
|
11
11
|
/** Controlled field values */
|
|
@@ -75,10 +75,6 @@ export function OpenFrameSsoSignUpForm({
|
|
|
75
75
|
|
|
76
76
|
// Validation messages are deferred while the user is typing (shown on blur or after a pause).
|
|
77
77
|
const emailErr = useDeferredError(errors?.email, email);
|
|
78
|
-
const firstNameErr = useDeferredError(errors?.firstName, firstName);
|
|
79
|
-
const lastNameErr = useDeferredError(errors?.lastName, lastName);
|
|
80
|
-
const passwordErr = useDeferredError(errors?.password, password);
|
|
81
|
-
const confirmErr = useDeferredError(errors?.confirmPassword, confirmPassword);
|
|
82
78
|
|
|
83
79
|
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
|
84
80
|
if (event.key === 'Enter' && !fieldsDisabled && !submitDisabled) {
|
|
@@ -111,45 +107,17 @@ export function OpenFrameSsoSignUpForm({
|
|
|
111
107
|
onKeyDown={handleKeyDown}
|
|
112
108
|
/>
|
|
113
109
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
/>
|
|
125
|
-
<Input
|
|
126
|
-
label="Last Name"
|
|
127
|
-
placeholder="Enter Last Name"
|
|
128
|
-
value={lastName}
|
|
129
|
-
error={lastNameErr.error}
|
|
130
|
-
disabled={fieldsDisabled}
|
|
131
|
-
onBlur={lastNameErr.onBlur}
|
|
132
|
-
onChange={event => onLastNameChange(event.target.value)}
|
|
133
|
-
onKeyDown={handleKeyDown}
|
|
134
|
-
/>
|
|
135
|
-
<PasswordInput
|
|
136
|
-
label="Password"
|
|
137
|
-
placeholder="Enter Password"
|
|
138
|
-
value={password}
|
|
139
|
-
error={passwordErr.error}
|
|
140
|
-
disabled={fieldsDisabled}
|
|
141
|
-
onBlur={passwordErr.onBlur}
|
|
142
|
-
onChange={event => onPasswordChange(event.target.value)}
|
|
143
|
-
onKeyDown={handleKeyDown}
|
|
144
|
-
/>
|
|
145
|
-
<PasswordInput
|
|
146
|
-
label="Confirm Password"
|
|
147
|
-
placeholder="Confirm Password"
|
|
148
|
-
value={confirmPassword}
|
|
149
|
-
error={confirmErr.error}
|
|
110
|
+
<AccountDetailsFields
|
|
111
|
+
firstName={firstName}
|
|
112
|
+
lastName={lastName}
|
|
113
|
+
password={password}
|
|
114
|
+
confirmPassword={confirmPassword}
|
|
115
|
+
onFirstNameChange={onFirstNameChange}
|
|
116
|
+
onLastNameChange={onLastNameChange}
|
|
117
|
+
onPasswordChange={onPasswordChange}
|
|
118
|
+
onConfirmPasswordChange={onConfirmPasswordChange}
|
|
119
|
+
errors={errors}
|
|
150
120
|
disabled={fieldsDisabled}
|
|
151
|
-
onBlur={confirmErr.onBlur}
|
|
152
|
-
onChange={event => onConfirmPasswordChange(event.target.value)}
|
|
153
121
|
onKeyDown={handleKeyDown}
|
|
154
122
|
/>
|
|
155
123
|
|
|
@@ -12,7 +12,7 @@ export type AuthSsoProvider = 'openframe' | 'google' | 'microsoft' | 'apple';
|
|
|
12
12
|
|
|
13
13
|
export const PROVIDER_META: Record<AuthSsoProvider, { name: string; Icon: ComponentType<{ className?: string }> }> = {
|
|
14
14
|
openframe: {
|
|
15
|
-
name: 'OpenFrame
|
|
15
|
+
name: 'OpenFrame',
|
|
16
16
|
Icon: ({ className }) => (
|
|
17
17
|
<OpenFrameLogo
|
|
18
18
|
className={className}
|
|
@@ -29,7 +29,7 @@ export const PROVIDER_META: Record<AuthSsoProvider, { name: string; Icon: Compon
|
|
|
29
29
|
export interface SsoProviderButtonsProps {
|
|
30
30
|
providers: AuthSsoProvider[];
|
|
31
31
|
onSsoClick?: (provider: AuthSsoProvider) => void;
|
|
32
|
-
/** Verb prefix for provider buttons, e.g. "Continue with".
|
|
32
|
+
/** Verb prefix for provider buttons, e.g. "Continue with". */
|
|
33
33
|
actionLabel?: string;
|
|
34
34
|
disabled?: boolean;
|
|
35
35
|
/**
|
|
@@ -65,7 +65,7 @@ export function SsoProviderButtons({
|
|
|
65
65
|
leftIcon={<Icon className="h-5 w-5" />}
|
|
66
66
|
onClick={() => onSsoClick?.(provider)}
|
|
67
67
|
>
|
|
68
|
-
{
|
|
68
|
+
{`${actionLabel} ${meta.name}`}
|
|
69
69
|
</Button>
|
|
70
70
|
);
|
|
71
71
|
})}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { ColumnDef, Table } from '@tanstack/react-table';
|
|
2
|
+
import { render, renderHook, screen } from '@testing-library/react';
|
|
3
|
+
import { memo } from 'react';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
import { DataTable } from '../data-table';
|
|
6
|
+
import { useDataTable } from '../data-table/use-data-table';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* What this pins down: `useDataTable` must hand back a NEW reference on every
|
|
10
|
+
* render, because `useReactTable` creates one instance for the life of the
|
|
11
|
+
* component and mutates it in place.
|
|
12
|
+
*
|
|
13
|
+
* Everything inside `<DataTable>` reads its rows back out of that instance
|
|
14
|
+
* through context, so a caller compiled by the React Compiler caches
|
|
15
|
+
* `<DataTable table={table}>` and its children on the table's identity — which
|
|
16
|
+
* never changes — and a table that gains rows renders nothing new. On an
|
|
17
|
+
* infinite-scroll table that is a request loop: the sentinel never moves down,
|
|
18
|
+
* so `DataTable.InfiniteFooter` keeps asking for the next page.
|
|
19
|
+
*
|
|
20
|
+
* `CachedTable` below stands in for that compiled caller. `memo` bails out on
|
|
21
|
+
* identical props exactly like the compiler's `if ($[n] !== table)` guard.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
interface Row {
|
|
25
|
+
id: string;
|
|
26
|
+
name: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const COLUMNS: ColumnDef<Row>[] = [{ accessorKey: 'name', header: 'Name', cell: ({ row }) => row.original.name }];
|
|
30
|
+
|
|
31
|
+
const getRowId = (row: Row) => row.id;
|
|
32
|
+
|
|
33
|
+
function rows(count: number): Row[] {
|
|
34
|
+
return Array.from({ length: count }, (_, i) => ({ id: `r${i}`, name: `row-${i}` }));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const CachedTable = memo(function CachedTableImpl({ table }: { table: Table<Row> }) {
|
|
38
|
+
return (
|
|
39
|
+
<DataTable table={table}>
|
|
40
|
+
<DataTable.Body<Row> />
|
|
41
|
+
</DataTable>
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
function TableHost({ data }: { data: Row[] }) {
|
|
46
|
+
const table = useDataTable<Row>({ data, columns: COLUMNS, getRowId });
|
|
47
|
+
return <CachedTable table={table} />;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('useDataTable identity', () => {
|
|
51
|
+
it('lets a consumer memoized on the table render rows that arrived later', () => {
|
|
52
|
+
const { rerender } = render(<TableHost data={rows(2)} />);
|
|
53
|
+
expect(screen.getAllByText(/^row-\d$/)).toHaveLength(2);
|
|
54
|
+
|
|
55
|
+
rerender(<TableHost data={rows(4)} />);
|
|
56
|
+
expect(screen.getAllByText(/^row-\d$/)).toHaveLength(4);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('forwards reads and writes to the single underlying instance', () => {
|
|
60
|
+
const { result, rerender } = renderHook(({ data }) => useDataTable<Row>({ data, columns: COLUMNS, getRowId }), {
|
|
61
|
+
initialProps: { data: rows(3) },
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const first = result.current;
|
|
65
|
+
expect(first.getRowModel().rows).toHaveLength(3);
|
|
66
|
+
// A write through one handle must be visible through the next one.
|
|
67
|
+
first.setOptions(prev => ({ ...prev, meta: { probed: true } }));
|
|
68
|
+
|
|
69
|
+
rerender({ data: rows(5) });
|
|
70
|
+
expect(result.current).not.toBe(first);
|
|
71
|
+
expect(result.current.getRowModel().rows).toHaveLength(5);
|
|
72
|
+
expect(result.current.options.meta).toEqual({ probed: true });
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -98,7 +98,12 @@ export function useDataTable<T>({
|
|
|
98
98
|
getFilteredRowModel: getFilteredRowModelOverride,
|
|
99
99
|
...rest
|
|
100
100
|
}: UseDataTableOptions<T>): Table<T> {
|
|
101
|
-
|
|
101
|
+
// Opted out of the React Compiler on purpose — see the handle published at the
|
|
102
|
+
// end of this function. Nothing here is worth memoizing anyway: `setOptions`
|
|
103
|
+
// runs on every render regardless.
|
|
104
|
+
'use no memo';
|
|
105
|
+
|
|
106
|
+
const table = useReactTable<T>({
|
|
102
107
|
...rest,
|
|
103
108
|
getCoreRowModel: coreRowModelFactory as TableOptions<T>['getCoreRowModel'],
|
|
104
109
|
getSortedRowModel: clientSideSorting
|
|
@@ -110,4 +115,34 @@ export function useDataTable<T>({
|
|
|
110
115
|
manualSorting: manualSorting ?? !clientSideSorting,
|
|
111
116
|
manualFiltering: manualFiltering ?? !clientSideFiltering,
|
|
112
117
|
});
|
|
118
|
+
|
|
119
|
+
// A fresh handle per render, and the reason this hook doesn't just return
|
|
120
|
+
// `useReactTable`'s value.
|
|
121
|
+
//
|
|
122
|
+
// `useReactTable` creates ONE instance for the life of the component and
|
|
123
|
+
// mutates it in place, so its identity is the same object whether a page of
|
|
124
|
+
// rows just arrived or nothing happened at all. Every consumer reads rows and
|
|
125
|
+
// state back out of that instance through context (`DataTable.Body`,
|
|
126
|
+
// `.Header`, `.RowCount`, `.CursorFooter`) — which worked only because the
|
|
127
|
+
// caller re-rendered and rebuilt their elements.
|
|
128
|
+
//
|
|
129
|
+
// Under the React Compiler it stops working, because the compiler caches on
|
|
130
|
+
// identity: `<DataTable table={table}>` and its children get cached on
|
|
131
|
+
// `table`, and inside the compiled `DataTableBody` the entire body —
|
|
132
|
+
// `table.getRowModel().rows` included — sits behind a `$[n] !== table` guard.
|
|
133
|
+
// A table that gains rows then renders nothing new. On an infinite-scroll
|
|
134
|
+
// table that is a loop: the sentinel never moves down, so
|
|
135
|
+
// `DataTable.InfiniteFooter` asks for the next page forever.
|
|
136
|
+
//
|
|
137
|
+
// The proxy forwards every read, write and TanStack mutation to that single
|
|
138
|
+
// instance, so nothing about the table's behaviour changes — only its
|
|
139
|
+
// identity, which is what puts it back inside React's reactive graph.
|
|
140
|
+
//
|
|
141
|
+
// The `use no memo` at the top of this hook is what keeps it a FRESH handle:
|
|
142
|
+
// without it the compiler caches this very object on `table` and hands back
|
|
143
|
+
// the same one forever, i.e. the bug. Keying a `useMemo` on
|
|
144
|
+
// `getRowModel()`/`getState()` instead is not better — `useReactTable`
|
|
145
|
+
// rebuilds `options.state` on every render anyway, so it would republish just
|
|
146
|
+
// as often while reading as though it did not.
|
|
147
|
+
return new Proxy(table, {});
|
|
113
148
|
}
|
|
@@ -48,8 +48,7 @@ function LoginPage(initial: Partial<LoginFormProps>) {
|
|
|
48
48
|
email={email}
|
|
49
49
|
onEmailChange={setEmail}
|
|
50
50
|
submitDisabled={!isValid}
|
|
51
|
-
|
|
52
|
-
onForgotPassword={() => {}}
|
|
51
|
+
onSubmitClick={() => {}}
|
|
53
52
|
/>
|
|
54
53
|
) : (
|
|
55
54
|
<div className="flex min-h-[240px] items-center justify-center rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-xl)] text-ods-text-secondary text-h4">
|
|
@@ -70,9 +69,20 @@ export const Filled: Story = {
|
|
|
70
69
|
render: () => <LoginPage email="roman@mail.com" />,
|
|
71
70
|
};
|
|
72
71
|
|
|
73
|
-
|
|
72
|
+
/** The generic providers, which need nothing typed and render above the email field. */
|
|
73
|
+
const SSO_PROVIDERS: AuthSsoProvider[] = ['google', 'microsoft', 'apple'];
|
|
74
74
|
|
|
75
|
-
/**
|
|
75
|
+
/** Providers above the email field, before anything is typed. */
|
|
76
76
|
export const SSO: Story = {
|
|
77
|
-
render: () => <LoginPage
|
|
77
|
+
render: () => <LoginPage ssoProviders={SSO_PROVIDERS} />,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/** Discovery resolved a tenant that has its own SSO: its options appear under the email. */
|
|
81
|
+
export const CustomSso: Story = {
|
|
82
|
+
render: () => <LoginPage email="roman@mail.com" ssoProviders={SSO_PROVIDERS} customSsoProviders={['openframe']} />,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** Discovery resolved a tenant with none configured — the empty array is the answer, not silence. */
|
|
86
|
+
export const NoCustomSso: Story = {
|
|
87
|
+
render: () => <LoginPage email="roman@mail.com" ssoProviders={SSO_PROVIDERS} customSsoProviders={[]} />,
|
|
78
88
|
};
|