@nocobase/app-template-default 0.0.0-stage → 1.0.0-beta.57
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/.cursor/mcp.json +8 -0
- package/.mcp.json +8 -0
- package/.prettierignore +20 -0
- package/.vscode/mcp.json +8 -0
- package/AGENTS.md +432 -0
- package/CLAUDE.md +9 -0
- package/Dockerfile +132 -0
- package/Dockerfile.dockerignore +45 -0
- package/README.MD +481 -0
- package/cli/plugins.ts +14 -0
- package/client/agent-annotations-host.ts +19 -0
- package/client/app.ts +38 -0
- package/client/components/back-button.tsx +49 -0
- package/client/components/breadcrumbs.tsx +167 -0
- package/client/components/inbox-badge.ts +49 -0
- package/client/components/inbox-button.tsx +132 -0
- package/client/components/inbox-header-button.tsx +25 -0
- package/client/components/loading.tsx +35 -0
- package/client/components/page-container.tsx +24 -0
- package/client/components/page-header.tsx +33 -0
- package/client/components/route-child-page.tsx +121 -0
- package/client/components/route-dialog.tsx +10 -0
- package/client/components/route-drawer.tsx +10 -0
- package/client/components/route-overlay.tsx +190 -0
- package/client/components/ui/alert.tsx +75 -0
- package/client/components/ui/badge.tsx +51 -0
- package/client/components/ui/breadcrumb.tsx +121 -0
- package/client/components/ui/button.tsx +58 -0
- package/client/components/ui/collapsible.tsx +19 -0
- package/client/components/ui/context-menu.tsx +271 -0
- package/client/components/ui/dialog.tsx +160 -0
- package/client/components/ui/dropdown-menu.tsx +270 -0
- package/client/components/ui/empty.tsx +103 -0
- package/client/components/ui/field.tsx +236 -0
- package/client/components/ui/input-group.tsx +158 -0
- package/client/components/ui/input.tsx +20 -0
- package/client/components/ui/kbd.tsx +26 -0
- package/client/components/ui/label.tsx +18 -0
- package/client/components/ui/popover.tsx +89 -0
- package/client/components/ui/select.tsx +200 -0
- package/client/components/ui/separator.tsx +22 -0
- package/client/components/ui/sheet.tsx +137 -0
- package/client/components/ui/sidebar.tsx +723 -0
- package/client/components/ui/skeleton.tsx +13 -0
- package/client/components/ui/spinner.tsx +20 -0
- package/client/components/ui/tabs.tsx +79 -0
- package/client/components/ui/textarea.tsx +17 -0
- package/client/components/ui/toast.tsx +231 -0
- package/client/components/ui/tooltip.tsx +63 -0
- package/client/components/use-route-overlay.ts +26 -0
- package/client/config/api.ts +13 -0
- package/client/config/app.ts +8 -0
- package/client/config/auth.ts +18 -0
- package/client/config/index.ts +17 -0
- package/client/extensions/nocobase-auth-forms/field-props.ts +13 -0
- package/client/extensions/nocobase-auth-forms/form-parts.tsx +101 -0
- package/client/extensions/nocobase-auth-forms/password-input.tsx +41 -0
- package/client/extensions/nocobase-auth-forms/password-login-form.tsx +123 -0
- package/client/extensions/nocobase-auth-forms/password-registration-form.tsx +193 -0
- package/client/extensions/nocobase-auth-forms/password-reset-form.tsx +132 -0
- package/client/extensions/nocobase-auth-forms/password-reset-request-form.tsx +99 -0
- package/client/extensions/nocobase-auth-methods/auth-methods.tsx +170 -0
- package/client/extensions/nocobase-auth-split-layout/auth-split-layout.tsx +126 -0
- package/client/extensions/nocobase-device-approval/device-approval.tsx +277 -0
- package/client/extensions/nocobase-device-approval/locales/en-US.ts +55 -0
- package/client/extensions/nocobase-device-approval/locales/zh-CN.ts +27 -0
- package/client/extensions/nocobase-device-approval/use-device-approval.ts +176 -0
- package/client/extensions/nocobase-file-component-ui/README.md +13 -0
- package/client/extensions/nocobase-file-component-ui/components/file-list.tsx +147 -0
- package/client/extensions/nocobase-file-component-ui/components/file-preview-dialog.tsx +263 -0
- package/client/extensions/nocobase-file-component-ui/components/file-preview-field.tsx +68 -0
- package/client/extensions/nocobase-file-component-ui/components/file-thumbnail.tsx +73 -0
- package/client/extensions/nocobase-file-component-ui/components/file-upload-field.tsx +386 -0
- package/client/extensions/nocobase-file-component-ui/components/previewers/file-preview-content.tsx +255 -0
- package/client/extensions/nocobase-file-component-ui/components/previewers/office-open-xml-preview.tsx +226 -0
- package/client/extensions/nocobase-file-component-ui/index.ts +8 -0
- package/client/extensions/nocobase-file-component-ui/lib/file-preview.ts +194 -0
- package/client/extensions/nocobase-file-component-ui/lib/file-url.ts +22 -0
- package/client/extensions/nocobase-file-component-ui/lib/message-key.ts +18 -0
- package/client/extensions/nocobase-file-component-ui/types.ts +69 -0
- package/client/extensions/nocobase-inbox/decision-actions-bar.tsx +140 -0
- package/client/extensions/nocobase-inbox/frame.ts +31 -0
- package/client/extensions/nocobase-inbox/inbox-actions.ts +21 -0
- package/client/extensions/nocobase-inbox/inbox-detail.tsx +386 -0
- package/client/extensions/nocobase-inbox/inbox-item.tsx +274 -0
- package/client/extensions/nocobase-inbox/inbox-list.tsx +433 -0
- package/client/extensions/nocobase-inbox/inbox-page.tsx +545 -0
- package/client/extensions/nocobase-inbox/locales/en-US.ts +111 -0
- package/client/extensions/nocobase-inbox/locales/zh-CN.ts +57 -0
- package/client/extensions/nocobase-inbox/model.ts +428 -0
- package/client/extensions/nocobase-inbox/registry-scope.tsx +49 -0
- package/client/extensions/nocobase-inbox/registry.ts +303 -0
- package/client/extensions/nocobase-inbox/source.ts +117 -0
- package/client/hooks/use-mobile.ts +21 -0
- package/client/index.tsx +60 -0
- package/client/layouts/app-layout.tsx +105 -0
- package/client/layouts/components/app-brand.tsx +49 -0
- package/client/layouts/components/app-sidebar.tsx +196 -0
- package/client/layouts/components/header-actions.tsx +50 -0
- package/client/layouts/components/language-switcher.tsx +95 -0
- package/client/layouts/components/layout-header.tsx +17 -0
- package/client/layouts/components/navigation-menu.tsx +411 -0
- package/client/layouts/components/sidebar-footer.tsx +107 -0
- package/client/layouts/components/surface-empty.tsx +35 -0
- package/client/layouts/components/user-menu.tsx +127 -0
- package/client/layouts/index.ts +2 -0
- package/client/layouts/settings-layout.tsx +172 -0
- package/client/layouts/use-sidebar-preference.ts +57 -0
- package/client/lib/constants.ts +5 -0
- package/client/lib/toaster.ts +56 -0
- package/client/lib/utils.ts +1 -0
- package/client/locales/en-US.ts +178 -0
- package/client/locales/index.ts +9 -0
- package/client/locales/zh-CN.ts +161 -0
- package/client/pages/auth/device.tsx +41 -0
- package/client/pages/auth/forgot-password.tsx +39 -0
- package/client/pages/auth/login.tsx +66 -0
- package/client/pages/auth/register.tsx +46 -0
- package/client/pages/auth/reset-password.tsx +38 -0
- package/client/pages/auth/shared.tsx +111 -0
- package/client/pages/home.tsx +16 -0
- package/client/pages/inbox.tsx +12 -0
- package/client/plugins.ts +41 -0
- package/client/react-providers.ts +27 -0
- package/client/route-overrides.ts +9 -0
- package/client/routes.ts +75 -0
- package/client/routing/app-router.tsx +127 -0
- package/client/routing/client-route.tsx +129 -0
- package/client/routing/route-context.tsx +85 -0
- package/client/routing/route-navigation.ts +153 -0
- package/client/routing/route-tree.tsx +42 -0
- package/client/routing/standalone-page-layout.tsx +15 -0
- package/client/runtime.ts +26 -0
- package/client/service-provider.ts +40 -0
- package/client/source-extensions.ts +19 -0
- package/client/startup.tsx +22 -0
- package/client/styles.css +122 -0
- package/client/theme/index.ts +4 -0
- package/client/theme/theme-context.ts +12 -0
- package/client/theme/theme-preferences.ts +84 -0
- package/client/theme/theme-presets.ts +7 -0
- package/client/theme/theme-provider.tsx +106 -0
- package/client/theme/theme-settings.tsx +176 -0
- package/client/theme/themes/compact.css +140 -0
- package/client/theme/themes/default.css +117 -0
- package/client/vite-env.d.ts +5 -0
- package/components.json +27 -0
- package/config.example.yml +172 -0
- package/database/main/migrations/.gitkeep +0 -0
- package/database/main/migrations/202610060001_create_device_code.ts +37 -0
- package/database/main/seeds/.gitkeep +0 -0
- package/database/tsconfig.json +4 -0
- package/ecosystem.config.js +35 -0
- package/eslint.config.js +24 -0
- package/index.html +100 -0
- package/package.json +200 -3
- package/playwright.config.ts +10 -0
- package/public/assets/favicon.ico +0 -0
- package/public/assets/logo-dark.png +0 -0
- package/public/assets/logo-mark-dark.png +0 -0
- package/public/assets/logo-mark.png +0 -0
- package/public/assets/logo.png +0 -0
- package/server/app.ts +50 -0
- package/server/config/ai.ts +27 -0
- package/server/config/api.ts +8 -0
- package/server/config/app.ts +40 -0
- package/server/config/auth.ts +36 -0
- package/server/config/authorization.ts +18 -0
- package/server/config/caching.ts +21 -0
- package/server/config/database.ts +22 -0
- package/server/config/drive.ts +30 -0
- package/server/config/i18n.ts +17 -0
- package/server/config/index.ts +73 -0
- package/server/config/jobs.ts +27 -0
- package/server/config/logging.ts +23 -0
- package/server/config/notification.ts +11 -0
- package/server/config/queue.ts +28 -0
- package/server/config/scheduler.ts +23 -0
- package/server/config/secrets.ts +10 -0
- package/server/config/server.ts +26 -0
- package/server/config/session.ts +60 -0
- package/server/config/snowflake.ts +21 -0
- package/server/config/spa.ts +24 -0
- package/server/config/users.ts +11 -0
- package/server/config/workflow.ts +24 -0
- package/server/config.ts +24 -0
- package/server/embedded.ts +23 -0
- package/server/locales/en-US.ts +10 -0
- package/server/locales/index.ts +10 -0
- package/server/locales/zh-CN.ts +5 -0
- package/server/plugins.ts +41 -0
- package/server/providers/index.ts +5 -0
- package/server/routes/index.ts +6 -0
- package/server/runtime.ts +24 -0
- package/server/standalone.ts +29 -0
- package/server/tsconfig.json +4 -0
- package/tailwind.config.mjs +59 -0
- package/tests/components/app-layout-routes.test.tsx +132 -0
- package/tests/components/app-title.test.tsx +57 -0
- package/tests/components/auth-i18n.test.tsx +87 -0
- package/tests/components/back-button.test.tsx +80 -0
- package/tests/components/file-preview.test.tsx +101 -0
- package/tests/components/header-hover.test.tsx +87 -0
- package/tests/components/inbox.test.tsx +139 -0
- package/tests/components/language-switcher.test.tsx +298 -0
- package/tests/components/layout-containers.test.tsx +15 -0
- package/tests/components/layout-i18n.test.tsx +187 -0
- package/tests/components/loading.test.tsx +25 -0
- package/tests/components/navigation-menu.test.tsx +355 -0
- package/tests/components/page-harness.test.tsx +318 -0
- package/tests/components/primitive-labels.test.tsx +80 -0
- package/tests/components/sidebar-permissions.test.tsx +110 -0
- package/tests/components/sidebar-preference.test.tsx +42 -0
- package/tests/fixtures/theme-tokens.html +12 -0
- package/tests/fixtures/theme-tokens.tsx +111 -0
- package/tests/logic/account-permissions.test.tsx +204 -0
- package/tests/logic/app-locale-coverage.test.ts +136 -0
- package/tests/logic/app-server.test.ts +1589 -0
- package/tests/logic/asset-url-build.test.ts +106 -0
- package/tests/logic/client-auth.test.tsx +122 -0
- package/tests/logic/client-plugin-registry.test.ts +63 -0
- package/tests/logic/client-route-recovery.test.tsx +80 -0
- package/tests/logic/client-routes.test.ts +126 -0
- package/tests/logic/client-runtime.test.ts +112 -0
- package/tests/logic/client-settings.test.tsx +841 -0
- package/tests/logic/client-shell.test.tsx +466 -0
- package/tests/logic/client-theme.test.tsx +441 -0
- package/tests/logic/config.test.ts +197 -0
- package/tests/logic/dev-client-watch.test.ts +108 -0
- package/tests/logic/dev-routes-production-build.test.ts +168 -0
- package/tests/logic/device-code-migration.test.ts +70 -0
- package/tests/logic/lifecycle.test.ts +221 -0
- package/tests/logic/plugin-commands.test.ts +69 -0
- package/tests/logic/plugin-page-authz.test.ts +76 -0
- package/tests/logic/route-navigation.test.ts +66 -0
- package/tests/logic/route-overlay.test.tsx +353 -0
- package/tests/logic/server-imports.test.ts +88 -0
- package/tests/logic/tailwind-sources.test.ts +151 -0
- package/tests/logic/template-package-name.test.ts +88 -0
- package/tests/logic/theme-preferences.test.ts +86 -0
- package/tests/logic/theme-tokens.test.ts +248 -0
- package/tests/logic/toaster.test.tsx +108 -0
- package/tests/logic/user-menu-sign-out.test.tsx +98 -0
- package/tests/setup/client-config.ts +27 -0
- package/tsconfig.json +11 -0
- package/tsconfig.node.json +12 -0
- package/tsconfig.server.json +19 -0
- package/vite.config.ts +92 -0
- package/vitest.config.ts +37 -0
- package/README.md +0 -3
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useId,
|
|
3
|
+
useState,
|
|
4
|
+
type FormEvent,
|
|
5
|
+
type ReactElement,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
} from 'react';
|
|
8
|
+
|
|
9
|
+
import { Button } from '#components/ui/button';
|
|
10
|
+
import { FieldGroup } from '#components/ui/field';
|
|
11
|
+
import { Input } from '#components/ui/input';
|
|
12
|
+
|
|
13
|
+
import { invalidProps } from './field-props.js';
|
|
14
|
+
import { AuthField, AuthFormStatus, AuthFormSubmit } from './form-parts.js';
|
|
15
|
+
import { PasswordInput } from './password-input.js';
|
|
16
|
+
|
|
17
|
+
export interface PasswordLoginValues {
|
|
18
|
+
readonly identifier: string;
|
|
19
|
+
readonly password: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface PasswordLoginLabels {
|
|
23
|
+
readonly identifier: string;
|
|
24
|
+
readonly password: string;
|
|
25
|
+
readonly submit: string;
|
|
26
|
+
readonly submitting: string;
|
|
27
|
+
readonly showPassword: string;
|
|
28
|
+
readonly hidePassword: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const defaultPasswordLoginLabels: PasswordLoginLabels = {
|
|
32
|
+
identifier: 'Username or email',
|
|
33
|
+
password: 'Password',
|
|
34
|
+
submit: 'Sign in',
|
|
35
|
+
submitting: 'Signing in…',
|
|
36
|
+
showPassword: 'Show password',
|
|
37
|
+
hidePassword: 'Hide password',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export interface PasswordLoginFormProps {
|
|
41
|
+
/** Called with the trimmed identifier and the password when the form is submitted. */
|
|
42
|
+
readonly onSubmit: (values: PasswordLoginValues) => void | Promise<void>;
|
|
43
|
+
/** Disables the submit button and shows `labels.submitting`. */
|
|
44
|
+
readonly submitting?: boolean;
|
|
45
|
+
/** A message about the whole attempt, such as wrong credentials. */
|
|
46
|
+
readonly error?: ReactNode;
|
|
47
|
+
readonly fieldErrors?: Partial<Record<keyof PasswordLoginValues, ReactNode>>;
|
|
48
|
+
readonly labels?: Partial<PasswordLoginLabels>;
|
|
49
|
+
/** Rendered under the password input, right-aligned, usually a link to the forgot-password page. It comes after the input in tab order. */
|
|
50
|
+
readonly forgotPasswordLink?: ReactNode;
|
|
51
|
+
/** Rendered under the submit button, usually a line linking to sign-up. */
|
|
52
|
+
readonly footer?: ReactNode;
|
|
53
|
+
readonly className?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Sign in with a username or email and a password. */
|
|
57
|
+
export function PasswordLoginForm({
|
|
58
|
+
className,
|
|
59
|
+
error,
|
|
60
|
+
fieldErrors,
|
|
61
|
+
footer,
|
|
62
|
+
forgotPasswordLink,
|
|
63
|
+
labels: labelOverrides,
|
|
64
|
+
onSubmit,
|
|
65
|
+
submitting = false,
|
|
66
|
+
}: PasswordLoginFormProps): ReactElement {
|
|
67
|
+
const labels = { ...defaultPasswordLoginLabels, ...labelOverrides };
|
|
68
|
+
const id = useId();
|
|
69
|
+
const identifierId = `${id}-identifier`;
|
|
70
|
+
const passwordId = `${id}-password`;
|
|
71
|
+
const [identifier, setIdentifier] = useState('');
|
|
72
|
+
const [password, setPassword] = useState('');
|
|
73
|
+
|
|
74
|
+
const handleSubmit = (event: FormEvent<HTMLFormElement>): void => {
|
|
75
|
+
event.preventDefault();
|
|
76
|
+
void onSubmit({ identifier: identifier.trim(), password });
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<form className={className} onSubmit={handleSubmit}>
|
|
81
|
+
<FieldGroup>
|
|
82
|
+
<AuthField
|
|
83
|
+
error={fieldErrors?.identifier}
|
|
84
|
+
id={identifierId}
|
|
85
|
+
label={labels.identifier}
|
|
86
|
+
>
|
|
87
|
+
<Input
|
|
88
|
+
autoComplete='username'
|
|
89
|
+
autoFocus
|
|
90
|
+
id={identifierId}
|
|
91
|
+
onChange={(event) => setIdentifier(event.target.value)}
|
|
92
|
+
required
|
|
93
|
+
value={identifier}
|
|
94
|
+
{...invalidProps(identifierId, fieldErrors?.identifier)}
|
|
95
|
+
/>
|
|
96
|
+
</AuthField>
|
|
97
|
+
<AuthField
|
|
98
|
+
below={forgotPasswordLink}
|
|
99
|
+
error={fieldErrors?.password}
|
|
100
|
+
id={passwordId}
|
|
101
|
+
label={labels.password}
|
|
102
|
+
>
|
|
103
|
+
<PasswordInput
|
|
104
|
+
autoComplete='current-password'
|
|
105
|
+
hideLabel={labels.hidePassword}
|
|
106
|
+
id={passwordId}
|
|
107
|
+
onChange={(event) => setPassword(event.target.value)}
|
|
108
|
+
required
|
|
109
|
+
showLabel={labels.showPassword}
|
|
110
|
+
value={password}
|
|
111
|
+
{...invalidProps(passwordId, fieldErrors?.password)}
|
|
112
|
+
/>
|
|
113
|
+
</AuthField>
|
|
114
|
+
{error ? <AuthFormStatus type='error'>{error}</AuthFormStatus> : null}
|
|
115
|
+
<AuthFormSubmit footer={footer}>
|
|
116
|
+
<Button className='w-full' disabled={submitting} type='submit'>
|
|
117
|
+
{submitting ? labels.submitting : labels.submit}
|
|
118
|
+
</Button>
|
|
119
|
+
</AuthFormSubmit>
|
|
120
|
+
</FieldGroup>
|
|
121
|
+
</form>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useId,
|
|
3
|
+
useState,
|
|
4
|
+
type FormEvent,
|
|
5
|
+
type ReactElement,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
} from 'react';
|
|
8
|
+
|
|
9
|
+
import { Button } from '#components/ui/button';
|
|
10
|
+
import { FieldGroup } from '#components/ui/field';
|
|
11
|
+
import { Input } from '#components/ui/input';
|
|
12
|
+
|
|
13
|
+
import { invalidProps } from './field-props.js';
|
|
14
|
+
import { AuthField, AuthFormStatus, AuthFormSubmit } from './form-parts.js';
|
|
15
|
+
|
|
16
|
+
export interface PasswordRegistrationValues {
|
|
17
|
+
readonly name: string;
|
|
18
|
+
readonly username: string;
|
|
19
|
+
readonly email: string;
|
|
20
|
+
readonly password: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface PasswordRegistrationLabels {
|
|
24
|
+
readonly name: string;
|
|
25
|
+
readonly username: string;
|
|
26
|
+
readonly email: string;
|
|
27
|
+
readonly password: string;
|
|
28
|
+
readonly confirmPassword: string;
|
|
29
|
+
readonly submit: string;
|
|
30
|
+
readonly submitting: string;
|
|
31
|
+
/** Shown under the confirmation when it differs from the password; the form checks this itself. */
|
|
32
|
+
readonly passwordMismatch: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const defaultPasswordRegistrationLabels: PasswordRegistrationLabels = {
|
|
36
|
+
name: 'Name',
|
|
37
|
+
username: 'Username',
|
|
38
|
+
email: 'Email',
|
|
39
|
+
password: 'Password',
|
|
40
|
+
confirmPassword: 'Confirm password',
|
|
41
|
+
submit: 'Create account',
|
|
42
|
+
submitting: 'Creating account…',
|
|
43
|
+
passwordMismatch: "Passwords don't match.",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export interface PasswordRegistrationFormProps {
|
|
47
|
+
/** Called once the password and its confirmation match. */
|
|
48
|
+
readonly onSubmit: (
|
|
49
|
+
values: PasswordRegistrationValues,
|
|
50
|
+
) => void | Promise<void>;
|
|
51
|
+
readonly submitting?: boolean;
|
|
52
|
+
readonly error?: ReactNode;
|
|
53
|
+
readonly fieldErrors?: Partial<
|
|
54
|
+
Record<keyof PasswordRegistrationValues, ReactNode>
|
|
55
|
+
>;
|
|
56
|
+
readonly labels?: Partial<PasswordRegistrationLabels>;
|
|
57
|
+
/** Rendered under the submit button, usually a line linking back to sign-in. */
|
|
58
|
+
readonly footer?: ReactNode;
|
|
59
|
+
readonly className?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Create an account with a name, username, email and password. */
|
|
63
|
+
export function PasswordRegistrationForm({
|
|
64
|
+
className,
|
|
65
|
+
error,
|
|
66
|
+
fieldErrors,
|
|
67
|
+
footer,
|
|
68
|
+
labels: labelOverrides,
|
|
69
|
+
onSubmit,
|
|
70
|
+
submitting = false,
|
|
71
|
+
}: PasswordRegistrationFormProps): ReactElement {
|
|
72
|
+
const labels = { ...defaultPasswordRegistrationLabels, ...labelOverrides };
|
|
73
|
+
const id = useId();
|
|
74
|
+
const ids = {
|
|
75
|
+
name: `${id}-name`,
|
|
76
|
+
username: `${id}-username`,
|
|
77
|
+
email: `${id}-email`,
|
|
78
|
+
password: `${id}-password`,
|
|
79
|
+
confirmation: `${id}-confirmation`,
|
|
80
|
+
};
|
|
81
|
+
const [values, setValues] = useState({
|
|
82
|
+
name: '',
|
|
83
|
+
username: '',
|
|
84
|
+
email: '',
|
|
85
|
+
password: '',
|
|
86
|
+
confirmation: '',
|
|
87
|
+
});
|
|
88
|
+
const [mismatch, setMismatch] = useState(false);
|
|
89
|
+
const set =
|
|
90
|
+
(field: keyof typeof values) =>
|
|
91
|
+
(event: { target: { value: string } }): void =>
|
|
92
|
+
setValues((current) => ({ ...current, [field]: event.target.value }));
|
|
93
|
+
|
|
94
|
+
const handleSubmit = (event: FormEvent<HTMLFormElement>): void => {
|
|
95
|
+
event.preventDefault();
|
|
96
|
+
if (values.password !== values.confirmation) {
|
|
97
|
+
setMismatch(true);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
setMismatch(false);
|
|
101
|
+
void onSubmit({
|
|
102
|
+
name: values.name.trim(),
|
|
103
|
+
username: values.username.trim(),
|
|
104
|
+
email: values.email.trim(),
|
|
105
|
+
password: values.password,
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const confirmationError = mismatch ? labels.passwordMismatch : undefined;
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<form className={className} onSubmit={handleSubmit}>
|
|
113
|
+
<FieldGroup>
|
|
114
|
+
<AuthField error={fieldErrors?.name} id={ids.name} label={labels.name}>
|
|
115
|
+
<Input
|
|
116
|
+
autoComplete='name'
|
|
117
|
+
autoFocus
|
|
118
|
+
id={ids.name}
|
|
119
|
+
onChange={set('name')}
|
|
120
|
+
required
|
|
121
|
+
value={values.name}
|
|
122
|
+
{...invalidProps(ids.name, fieldErrors?.name)}
|
|
123
|
+
/>
|
|
124
|
+
</AuthField>
|
|
125
|
+
<AuthField
|
|
126
|
+
error={fieldErrors?.username}
|
|
127
|
+
id={ids.username}
|
|
128
|
+
label={labels.username}
|
|
129
|
+
>
|
|
130
|
+
<Input
|
|
131
|
+
autoComplete='username'
|
|
132
|
+
id={ids.username}
|
|
133
|
+
onChange={set('username')}
|
|
134
|
+
required
|
|
135
|
+
value={values.username}
|
|
136
|
+
{...invalidProps(ids.username, fieldErrors?.username)}
|
|
137
|
+
/>
|
|
138
|
+
</AuthField>
|
|
139
|
+
<AuthField
|
|
140
|
+
error={fieldErrors?.email}
|
|
141
|
+
id={ids.email}
|
|
142
|
+
label={labels.email}
|
|
143
|
+
>
|
|
144
|
+
<Input
|
|
145
|
+
autoComplete='email'
|
|
146
|
+
id={ids.email}
|
|
147
|
+
onChange={set('email')}
|
|
148
|
+
required
|
|
149
|
+
type='email'
|
|
150
|
+
value={values.email}
|
|
151
|
+
{...invalidProps(ids.email, fieldErrors?.email)}
|
|
152
|
+
/>
|
|
153
|
+
</AuthField>
|
|
154
|
+
<AuthField
|
|
155
|
+
error={fieldErrors?.password}
|
|
156
|
+
id={ids.password}
|
|
157
|
+
label={labels.password}
|
|
158
|
+
>
|
|
159
|
+
<Input
|
|
160
|
+
autoComplete='new-password'
|
|
161
|
+
id={ids.password}
|
|
162
|
+
onChange={set('password')}
|
|
163
|
+
required
|
|
164
|
+
type='password'
|
|
165
|
+
value={values.password}
|
|
166
|
+
{...invalidProps(ids.password, fieldErrors?.password)}
|
|
167
|
+
/>
|
|
168
|
+
</AuthField>
|
|
169
|
+
<AuthField
|
|
170
|
+
error={confirmationError}
|
|
171
|
+
id={ids.confirmation}
|
|
172
|
+
label={labels.confirmPassword}
|
|
173
|
+
>
|
|
174
|
+
<Input
|
|
175
|
+
autoComplete='new-password'
|
|
176
|
+
id={ids.confirmation}
|
|
177
|
+
onChange={set('confirmation')}
|
|
178
|
+
required
|
|
179
|
+
type='password'
|
|
180
|
+
value={values.confirmation}
|
|
181
|
+
{...invalidProps(ids.confirmation, confirmationError)}
|
|
182
|
+
/>
|
|
183
|
+
</AuthField>
|
|
184
|
+
{error ? <AuthFormStatus type='error'>{error}</AuthFormStatus> : null}
|
|
185
|
+
<AuthFormSubmit footer={footer}>
|
|
186
|
+
<Button className='w-full' disabled={submitting} type='submit'>
|
|
187
|
+
{submitting ? labels.submitting : labels.submit}
|
|
188
|
+
</Button>
|
|
189
|
+
</AuthFormSubmit>
|
|
190
|
+
</FieldGroup>
|
|
191
|
+
</form>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useId,
|
|
3
|
+
useState,
|
|
4
|
+
type FormEvent,
|
|
5
|
+
type ReactElement,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
} from 'react';
|
|
8
|
+
|
|
9
|
+
import { Button } from '#components/ui/button';
|
|
10
|
+
import { FieldGroup } from '#components/ui/field';
|
|
11
|
+
import { Input } from '#components/ui/input';
|
|
12
|
+
|
|
13
|
+
import { invalidProps } from './field-props.js';
|
|
14
|
+
import { AuthField, AuthFormStatus, AuthFormSubmit } from './form-parts.js';
|
|
15
|
+
|
|
16
|
+
export interface PasswordResetValues {
|
|
17
|
+
readonly password: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PasswordResetLabels {
|
|
21
|
+
readonly password: string;
|
|
22
|
+
readonly confirmPassword: string;
|
|
23
|
+
readonly submit: string;
|
|
24
|
+
readonly submitting: string;
|
|
25
|
+
/** Shown under the confirmation when it differs from the password; the form checks this itself. */
|
|
26
|
+
readonly passwordMismatch: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const defaultPasswordResetLabels: PasswordResetLabels = {
|
|
30
|
+
password: 'New password',
|
|
31
|
+
confirmPassword: 'Confirm new password',
|
|
32
|
+
submit: 'Reset password',
|
|
33
|
+
submitting: 'Resetting…',
|
|
34
|
+
passwordMismatch: "Passwords don't match.",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export interface PasswordResetFormProps {
|
|
38
|
+
/** Called once the new password and its confirmation match. */
|
|
39
|
+
readonly onSubmit: (values: PasswordResetValues) => void | Promise<void>;
|
|
40
|
+
readonly submitting?: boolean;
|
|
41
|
+
/** Disables every field, for example while the reset link is missing its token. */
|
|
42
|
+
readonly disabled?: boolean;
|
|
43
|
+
readonly error?: ReactNode;
|
|
44
|
+
readonly fieldErrors?: Partial<Record<keyof PasswordResetValues, ReactNode>>;
|
|
45
|
+
readonly labels?: Partial<PasswordResetLabels>;
|
|
46
|
+
readonly footer?: ReactNode;
|
|
47
|
+
readonly className?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Choose a new password, from the link a reset request sent. */
|
|
51
|
+
export function PasswordResetForm({
|
|
52
|
+
className,
|
|
53
|
+
disabled = false,
|
|
54
|
+
error,
|
|
55
|
+
fieldErrors,
|
|
56
|
+
footer,
|
|
57
|
+
labels: labelOverrides,
|
|
58
|
+
onSubmit,
|
|
59
|
+
submitting = false,
|
|
60
|
+
}: PasswordResetFormProps): ReactElement {
|
|
61
|
+
const labels = { ...defaultPasswordResetLabels, ...labelOverrides };
|
|
62
|
+
const id = useId();
|
|
63
|
+
const passwordId = `${id}-password`;
|
|
64
|
+
const confirmationId = `${id}-confirmation`;
|
|
65
|
+
const [password, setPassword] = useState('');
|
|
66
|
+
const [confirmation, setConfirmation] = useState('');
|
|
67
|
+
const [mismatch, setMismatch] = useState(false);
|
|
68
|
+
|
|
69
|
+
const handleSubmit = (event: FormEvent<HTMLFormElement>): void => {
|
|
70
|
+
event.preventDefault();
|
|
71
|
+
if (disabled) return;
|
|
72
|
+
if (password !== confirmation) {
|
|
73
|
+
setMismatch(true);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
setMismatch(false);
|
|
77
|
+
void onSubmit({ password });
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const confirmationError = mismatch ? labels.passwordMismatch : undefined;
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<form className={className} onSubmit={handleSubmit}>
|
|
84
|
+
<FieldGroup>
|
|
85
|
+
<AuthField
|
|
86
|
+
error={fieldErrors?.password}
|
|
87
|
+
id={passwordId}
|
|
88
|
+
label={labels.password}
|
|
89
|
+
>
|
|
90
|
+
<Input
|
|
91
|
+
autoComplete='new-password'
|
|
92
|
+
autoFocus
|
|
93
|
+
disabled={disabled}
|
|
94
|
+
id={passwordId}
|
|
95
|
+
onChange={(event) => setPassword(event.target.value)}
|
|
96
|
+
required
|
|
97
|
+
type='password'
|
|
98
|
+
value={password}
|
|
99
|
+
{...invalidProps(passwordId, fieldErrors?.password)}
|
|
100
|
+
/>
|
|
101
|
+
</AuthField>
|
|
102
|
+
<AuthField
|
|
103
|
+
error={confirmationError}
|
|
104
|
+
id={confirmationId}
|
|
105
|
+
label={labels.confirmPassword}
|
|
106
|
+
>
|
|
107
|
+
<Input
|
|
108
|
+
autoComplete='new-password'
|
|
109
|
+
disabled={disabled}
|
|
110
|
+
id={confirmationId}
|
|
111
|
+
onChange={(event) => setConfirmation(event.target.value)}
|
|
112
|
+
required
|
|
113
|
+
type='password'
|
|
114
|
+
value={confirmation}
|
|
115
|
+
{...invalidProps(confirmationId, confirmationError)}
|
|
116
|
+
/>
|
|
117
|
+
</AuthField>
|
|
118
|
+
{error ? <AuthFormStatus type='error'>{error}</AuthFormStatus> : null}
|
|
119
|
+
<AuthFormSubmit footer={footer}>
|
|
120
|
+
<Button
|
|
121
|
+
className='w-full'
|
|
122
|
+
disabled={disabled || submitting}
|
|
123
|
+
size='lg'
|
|
124
|
+
type='submit'
|
|
125
|
+
>
|
|
126
|
+
{submitting ? labels.submitting : labels.submit}
|
|
127
|
+
</Button>
|
|
128
|
+
</AuthFormSubmit>
|
|
129
|
+
</FieldGroup>
|
|
130
|
+
</form>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useId,
|
|
3
|
+
useState,
|
|
4
|
+
type FormEvent,
|
|
5
|
+
type ReactElement,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
} from 'react';
|
|
8
|
+
|
|
9
|
+
import { Button } from '#components/ui/button';
|
|
10
|
+
import { FieldGroup } from '#components/ui/field';
|
|
11
|
+
import { Input } from '#components/ui/input';
|
|
12
|
+
|
|
13
|
+
import { invalidProps } from './field-props.js';
|
|
14
|
+
import { AuthField, AuthFormStatus, AuthFormSubmit } from './form-parts.js';
|
|
15
|
+
|
|
16
|
+
export interface PasswordResetRequestValues {
|
|
17
|
+
readonly email: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PasswordResetRequestLabels {
|
|
21
|
+
readonly email: string;
|
|
22
|
+
readonly submit: string;
|
|
23
|
+
readonly submitting: string;
|
|
24
|
+
/** Shown while `success` is set. */
|
|
25
|
+
readonly success: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const defaultPasswordResetRequestLabels: PasswordResetRequestLabels = {
|
|
29
|
+
email: 'Email',
|
|
30
|
+
submit: 'Send reset link',
|
|
31
|
+
submitting: 'Sending…',
|
|
32
|
+
success: 'If the account exists, a reset link has been sent.',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export interface PasswordResetRequestFormProps {
|
|
36
|
+
readonly onSubmit: (
|
|
37
|
+
values: PasswordResetRequestValues,
|
|
38
|
+
) => void | Promise<void>;
|
|
39
|
+
readonly submitting?: boolean;
|
|
40
|
+
/** Shows `labels.success` once the request has been accepted. */
|
|
41
|
+
readonly success?: boolean;
|
|
42
|
+
readonly error?: ReactNode;
|
|
43
|
+
readonly fieldErrors?: Partial<
|
|
44
|
+
Record<keyof PasswordResetRequestValues, ReactNode>
|
|
45
|
+
>;
|
|
46
|
+
readonly labels?: Partial<PasswordResetRequestLabels>;
|
|
47
|
+
readonly footer?: ReactNode;
|
|
48
|
+
readonly className?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Ask for a password reset link by email. */
|
|
52
|
+
export function PasswordResetRequestForm({
|
|
53
|
+
className,
|
|
54
|
+
error,
|
|
55
|
+
fieldErrors,
|
|
56
|
+
footer,
|
|
57
|
+
labels: labelOverrides,
|
|
58
|
+
onSubmit,
|
|
59
|
+
submitting = false,
|
|
60
|
+
success = false,
|
|
61
|
+
}: PasswordResetRequestFormProps): ReactElement {
|
|
62
|
+
const labels = { ...defaultPasswordResetRequestLabels, ...labelOverrides };
|
|
63
|
+
const emailId = `${useId()}-email`;
|
|
64
|
+
const [email, setEmail] = useState('');
|
|
65
|
+
|
|
66
|
+
const handleSubmit = (event: FormEvent<HTMLFormElement>): void => {
|
|
67
|
+
event.preventDefault();
|
|
68
|
+
void onSubmit({ email: email.trim() });
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<form className={className} onSubmit={handleSubmit}>
|
|
73
|
+
<FieldGroup>
|
|
74
|
+
<AuthField error={fieldErrors?.email} id={emailId} label={labels.email}>
|
|
75
|
+
<Input
|
|
76
|
+
autoComplete='email'
|
|
77
|
+
autoFocus
|
|
78
|
+
id={emailId}
|
|
79
|
+
onChange={(event) => setEmail(event.target.value)}
|
|
80
|
+
required
|
|
81
|
+
type='email'
|
|
82
|
+
value={email}
|
|
83
|
+
{...invalidProps(emailId, fieldErrors?.email)}
|
|
84
|
+
/>
|
|
85
|
+
</AuthField>
|
|
86
|
+
{error ? (
|
|
87
|
+
<AuthFormStatus type='error'>{error}</AuthFormStatus>
|
|
88
|
+
) : success ? (
|
|
89
|
+
<AuthFormStatus type='success'>{labels.success}</AuthFormStatus>
|
|
90
|
+
) : null}
|
|
91
|
+
<AuthFormSubmit footer={footer}>
|
|
92
|
+
<Button className='w-full' disabled={submitting} type='submit'>
|
|
93
|
+
{submitting ? labels.submitting : labels.submit}
|
|
94
|
+
</Button>
|
|
95
|
+
</AuthFormSubmit>
|
|
96
|
+
</FieldGroup>
|
|
97
|
+
</form>
|
|
98
|
+
);
|
|
99
|
+
}
|