@nocobase/app-template-default 1.0.0-beta.60 → 1.0.0-beta.62
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/AGENTS.md +8 -0
- package/README.MD +2 -0
- package/client/components/ui/sidebar.tsx +1 -1
- package/client/locales/system/en-US.ts +52 -24
- package/client/locales/system/zh-CN.ts +50 -23
- package/client/pages/auth/forgot-password.tsx +51 -17
- package/client/pages/auth/login.tsx +8 -2
- package/client/pages/home/capabilities.ts +20 -10
- package/client/plugins.ts +2 -0
- package/config.example.yml +5 -0
- package/package.json +10 -8
- package/server/config/index.ts +3 -0
- package/server/config/mail.ts +3 -0
- package/server/plugins.ts +2 -0
- package/tests/components/auth-i18n.test.tsx +6 -0
- package/tests/logic/client-auth.test.tsx +77 -0
package/AGENTS.md
CHANGED
|
@@ -244,6 +244,14 @@ The languages the application offers are its own locale files, not a configured
|
|
|
244
244
|
|
|
245
245
|
The account menu language control in `client/layouts/components/language-switcher.tsx` uses a shadcn submenu with radio items. Render it inside `DropdownMenuContent` to preserve menu keyboard navigation and selection semantics.
|
|
246
246
|
|
|
247
|
+
## Application access URLs
|
|
248
|
+
|
|
249
|
+
When reporting or opening this application's address, use the `Local:` URL printed by `pnpm dev`, including the application base path. In normal full-stack development, the Hono server proxies Vite: the user-facing entry uses the server port (`APP_SERVER_PORT`, default 13000), while Vite's port (normally 5173) serves the frontend development process. A response from Vite alone does not verify the application entry.
|
|
250
|
+
|
|
251
|
+
For an existing run without available startup output, identify its process and working directory, confirm the actual server listening port and configured base path, and check that URL before sharing it. Occupied ports advance automatically, so do not assume defaults. Report when the run belongs to another checkout. Use a host reachable by the user; `localhost` refers to the machine where the browser runs.
|
|
252
|
+
|
|
253
|
+
With `PROXY_TARGET_URL`, the local Hono server is skipped and Vite becomes the local application entry. Use the printed `Local:` URL in this mode too; `APP_SERVER_PORT` selects its local port (default 5173). See the remote backend development guidance below.
|
|
254
|
+
|
|
247
255
|
## Development file watching
|
|
248
256
|
|
|
249
257
|
Changes to `.env` and `.env.local` (including creation, atomic replacement, and deletion) restart the full development run after its previous processes exit. The new run reloads environment files, ports, proxy settings, and startup hooks; explicit shell variables still take precedence. Use the newly printed URL if the port or base path changes. This also applies in proxy mode. `config.yml` changes restart only the local server. `NOCOBASE_STRICT_STARTUP=true` disables both automatic restarts.
|
package/README.MD
CHANGED
|
@@ -167,6 +167,8 @@ Database tasks support `--connection <name>` and `--all`; no argument targets `d
|
|
|
167
167
|
|
|
168
168
|
### Plugins
|
|
169
169
|
|
|
170
|
+
Mail (`@nocobase/app-plugin-mail`) is included and registered on the client and server. Configure mailbox providers under `mail.providers` in `config.yml` before connecting accounts; no provider credentials or demo mailboxes are included. See the [Mail configuration guide](.agents/skills/nocobase-app-plugin-mail/references/configuration-and-accounts.md). The plugin provides services and UI components; applications compose their own mailbox pages.
|
|
171
|
+
|
|
170
172
|
Plugins add features to your application. Let these commands edit `package.json`, `client/plugins.ts`, and `server/plugins.ts` for you rather than editing those files by hand.
|
|
171
173
|
|
|
172
174
|
| Command | What it does |
|
|
@@ -679,7 +679,7 @@ function SidebarMenuSubButton({
|
|
|
679
679
|
props: mergeProps<'a'>(
|
|
680
680
|
{
|
|
681
681
|
className: cn(
|
|
682
|
-
'flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground ring-sidebar-ring outline-hidden group-data-[collapsible=icon]:hidden hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[size=md]:text-sm data-[size=sm]:text-xs data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0
|
|
682
|
+
'flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground ring-sidebar-ring outline-hidden group-data-[collapsible=icon]:hidden hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[size=md]:text-sm data-[size=sm]:text-xs data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
|
|
683
683
|
className,
|
|
684
684
|
),
|
|
685
685
|
},
|
|
@@ -25,6 +25,11 @@ const systemEnUS = {
|
|
|
25
25
|
'auth.forgotTitle': 'Forgot password',
|
|
26
26
|
'auth.forgotDescription':
|
|
27
27
|
'Enter your email and we will send a reset link if the account exists.',
|
|
28
|
+
'auth.passwordResetUnavailable':
|
|
29
|
+
'Self-service password reset is not enabled. Contact an administrator to reset your password.',
|
|
30
|
+
'auth.capabilityLoadFailed': 'Could not check password reset availability.',
|
|
31
|
+
'auth.retry': 'Retry',
|
|
32
|
+
'auth.loading': 'Loading',
|
|
28
33
|
'auth.resetDescription': 'Choose a new password for your account.',
|
|
29
34
|
'auth.resetTitle': 'Reset password',
|
|
30
35
|
'auth.identifier': 'Username or email',
|
|
@@ -135,14 +140,29 @@ const systemEnUS = {
|
|
|
135
140
|
label: 'Close sign-up',
|
|
136
141
|
text: 'This application is for company employees only. Administrators create all accounts.\nDisable self-registration and remove the registration link from the sign-in page.\nExisting employees must still be able to sign in with their accounts and passwords.\nKeep the forgot-password link.',
|
|
137
142
|
},
|
|
138
|
-
passwordRules: {
|
|
139
|
-
label: 'Password rules',
|
|
140
|
-
text: 'Require at least 12 characters when registering or resetting a password.\nShow this requirement in both forms and use consistent validation messages.\nKeep existing accounts able to sign in without requiring an immediate password change.',
|
|
141
|
-
},
|
|
142
143
|
companySso: {
|
|
143
144
|
label: 'Company SSO',
|
|
144
145
|
text: "Let employees sign in with our company identity platform using OIDC.\n\nKeep account-password sign-in and add a Company account button.\nThe button opens the company identity platform. After successful sign-in, return to the application's home page.\n\nAllow first sign-in to create an application account, identified by the platform's stable user identifier.\nDo not automatically link accounts by matching names or emails. New accounts use the application's default permissions.\n\nList the information the identity platform administrator must provide and the callback URL they need to register.",
|
|
145
146
|
},
|
|
147
|
+
apiKeys: {
|
|
148
|
+
label: 'API keys',
|
|
149
|
+
text: "Add an Integration keys entry to the user menu, using the application's built-in API keys capability.\nEmployees manage only the keys they created, entering a purpose and a validity period in days.\nShow the full key only once after creation, with a prompt to copy and store it right away.\nList each key's name, an identifiable fragment, status, expiry, and last use, and allow revoking it after confirmation.\nNever show the full key in the list, logs, or details.",
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
dataSources: {
|
|
154
|
+
title: 'Data sources',
|
|
155
|
+
description:
|
|
156
|
+
'Choose the application database and use business data from existing external databases.',
|
|
157
|
+
prompts: {
|
|
158
|
+
postgres: {
|
|
159
|
+
label: 'Switch database',
|
|
160
|
+
text: "Switch the application database from SQLite to PostgreSQL.\nRead the connection details from the server's runtime configuration, not from source code.\nExplain which database driver to install and which settings to fill in, and remind me that existing data is not migrated automatically.",
|
|
161
|
+
},
|
|
162
|
+
externalDatabase: {
|
|
163
|
+
label: 'External database',
|
|
164
|
+
text: "Connect the company's existing finance database as a read-only external data source; do not modify its tables.\nMatch records by supplier number and add a Settlement section to supplier details showing the settlement cycle, credit limit, and settlement status, labeled with its source.\nKeep reading supplier contact details from the application database, and read settlement data directly from the external database without importing it.\nRead the connection credentials from the server's runtime configuration.",
|
|
165
|
+
},
|
|
146
166
|
},
|
|
147
167
|
},
|
|
148
168
|
authorization: {
|
|
@@ -158,9 +178,28 @@ const systemEnUS = {
|
|
|
158
178
|
label: 'Data scopes',
|
|
159
179
|
text: "Let purchase requesters view only their own orders, using the order's requester field to determine ownership.\nOrder reviewers can view all orders and process pending approvals.\nAdministrators can adjust each job's order-viewing scope in its permission set.",
|
|
160
180
|
},
|
|
161
|
-
|
|
162
|
-
label: '
|
|
163
|
-
text:
|
|
181
|
+
restrictions: {
|
|
182
|
+
label: 'Restrictions',
|
|
183
|
+
text: 'Bob is temporarily helping review purchase orders and may view only orders of 2,000 or less.\nAdd an amount limit to his View orders action, keeping his existing job and data scopes.\nApply the limit to his own orders and to orders shared with him.\nAdministrators can change the amount or remove the rule; before removal, explain that his visible scope may grow.',
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
workflow: {
|
|
188
|
+
title: 'Workflows',
|
|
189
|
+
description:
|
|
190
|
+
'Move business records through stages, with waiting, timeouts, retries, and a history of who did what.',
|
|
191
|
+
prompts: {
|
|
192
|
+
stages: {
|
|
193
|
+
label: 'Stages',
|
|
194
|
+
text: "Add a process to purchase orders: Pending approval → Pending purchase → Received → Completed.\nReviewers approve or reject; a rejection requires a reason and returns the order to the applicant to revise and resubmit.\nAfter the buyer places the purchase, the order is pending purchase; confirming delivery with a signed receipt moves it to Received, and the applicant's confirmation completes it.\nOrder details offer only the actions of the current stage, and ordinary edits cannot change the stage.\nCheck stages and required information on the server, and never overwrite a result someone else has already submitted.",
|
|
195
|
+
},
|
|
196
|
+
backgroundExport: {
|
|
197
|
+
label: 'Background export',
|
|
198
|
+
text: 'Add an order report page that exports existing orders to CSV.\nAfter submission the export waits to be processed, is generated in the background, and completes with a download link.\nShow the current stage, filename, number of exported orders, and the submission and system processing records.\nRetry failed generation according to rules, and keep a final failure for the owner to handle; running it again must not create duplicate report files.\nKeep the progress and result visible after a page refresh, and allow only authorized people to export orders.',
|
|
199
|
+
},
|
|
200
|
+
history: {
|
|
201
|
+
label: 'History',
|
|
202
|
+
text: 'Show the current stage and processing history in order details.\nEach entry shows who acted, when, the action, the stage before and after, and any rejection reason or signed receipt.\nMark actions performed automatically as done by the system.\nKeep the progress and history after a page refresh, so the whole process can be reviewed once completed.',
|
|
164
203
|
},
|
|
165
204
|
},
|
|
166
205
|
},
|
|
@@ -199,13 +238,17 @@ const systemEnUS = {
|
|
|
199
238
|
description:
|
|
200
239
|
'Connect personal mailboxes, then read, reply to, and send business email.',
|
|
201
240
|
prompts: {
|
|
241
|
+
providers: {
|
|
242
|
+
label: 'Mail provider',
|
|
243
|
+
text: "Configure mailbox.org for the application in the server configuration's mail.providers:\n- IMAP incoming server: imap.mailbox.org, port 993, SSL/TLS.\n- SMTP outgoing server: smtp.mailbox.org, port 465, SSL/TLS.\nUsers enter their own email address, username, and password on the mailbox accounts page and choose the date to start the first synchronization from; never write mailbox passwords into configuration or source code.",
|
|
244
|
+
},
|
|
202
245
|
mailCenter: {
|
|
203
246
|
label: 'Mail center',
|
|
204
|
-
text: '
|
|
247
|
+
text: 'Mail is built into this application; reuse it instead of building it again or installing another plugin.\nAdd Mail center and Mailbox accounts pages to the navigation, using the mail workspace and mailbox accounts pages the mail capability provides.\nUsers can connect their own mailboxes, synchronize incoming messages, read messages and attachments, and reply, keeping the original subject and conversation context and addressing the reply to the original sender.\nEach user works with their own connected mailboxes.',
|
|
205
248
|
},
|
|
206
249
|
correspondence: {
|
|
207
250
|
label: 'Customer emails',
|
|
208
|
-
text: "Add a correspondence section to customer details
|
|
251
|
+
text: "Add a correspondence section to customer details, reusing the application's built-in mail reading and compose components.\nShow messages whose sender or recipient matches the current customer's contact email addresses. Sales representatives can read messages, view attachments, and reply using their own connected mailboxes.",
|
|
209
252
|
},
|
|
210
253
|
},
|
|
211
254
|
},
|
|
@@ -224,21 +267,6 @@ const systemEnUS = {
|
|
|
224
267
|
},
|
|
225
268
|
},
|
|
226
269
|
},
|
|
227
|
-
templatePrint: {
|
|
228
|
-
title: 'Template printing',
|
|
229
|
-
description:
|
|
230
|
-
'Fill business data into Word or Excel templates and generate files.',
|
|
231
|
-
prompts: {
|
|
232
|
-
approvalForm: {
|
|
233
|
-
label: 'Approval form',
|
|
234
|
-
text: "Add a “Generate approval form” action to order details.\n\nUse the Word template I provided to fill in the current order's number, purchase request, applicant, amount, approval result, and processing time.\nKeep the template's heading, table layout, and sign-off area. Name the output “Purchase-approval-order-number” and provide a Word download.\nAnyone with access to the order can generate its approval form. Use the current order data when generating the file.",
|
|
235
|
-
},
|
|
236
|
-
pdf: {
|
|
237
|
-
label: 'PDF output',
|
|
238
|
-
text: "Add PDF download for the purchase approval form, preserving the Word template's fonts, tables, and sign-off area.",
|
|
239
|
-
},
|
|
240
|
-
},
|
|
241
|
-
},
|
|
242
270
|
i18n: {
|
|
243
271
|
title: 'Languages',
|
|
244
272
|
description:
|
|
@@ -20,6 +20,10 @@ const systemZhCN: SystemResource = {
|
|
|
20
20
|
'auth.registerDescription': '创建账户以开始使用。',
|
|
21
21
|
'auth.forgotTitle': '忘记密码',
|
|
22
22
|
'auth.forgotDescription': '输入邮箱,如果账户存在,我们将发送重置链接。',
|
|
23
|
+
'auth.passwordResetUnavailable': '未开启自助找回,请联系管理员重置密码。',
|
|
24
|
+
'auth.capabilityLoadFailed': '无法检查自助找回是否已开启。',
|
|
25
|
+
'auth.retry': '重试',
|
|
26
|
+
'auth.loading': '加载中',
|
|
23
27
|
'auth.resetDescription': '为账户设置新密码。',
|
|
24
28
|
'auth.resetTitle': '重置密码',
|
|
25
29
|
'auth.identifier': '用户名或邮箱',
|
|
@@ -124,14 +128,28 @@ const systemZhCN: SystemResource = {
|
|
|
124
128
|
label: '关闭注册',
|
|
125
129
|
text: '这个应用只供公司员工使用,账号由管理员统一创建。\n请关闭自助注册,登录页不再显示注册入口。\n已有员工仍能使用原账号和密码登录,保留忘记密码入口。',
|
|
126
130
|
},
|
|
127
|
-
passwordRules: {
|
|
128
|
-
label: '密码规则',
|
|
129
|
-
text: '新注册和重置密码时,密码至少需要 12 个字符。\n在注册和重置密码表单中显示这条要求,并使用一致的校验提示。\n保留已有账号的登录能力,已有用户无需立即修改密码。',
|
|
130
|
-
},
|
|
131
131
|
companySso: {
|
|
132
132
|
label: '企业账号登录',
|
|
133
133
|
text: '请让员工使用公司的统一身份平台登录,采用 OIDC。\n\n保留原有账号密码登录,在登录页增加「企业账号登录」入口。\n点击后进入企业身份平台,登录成功后返回应用首页。\n\n首次登录允许创建应用账号,以身份平台的稳定用户标识识别用户。\n不要根据同名或相同邮箱自动关联已有账号,新账号沿用应用默认权限。\n\n请列出身份平台管理员需要提供的信息,以及需要登记的回调地址。',
|
|
134
134
|
},
|
|
135
|
+
apiKeys: {
|
|
136
|
+
label: '集成密钥',
|
|
137
|
+
text: '在用户菜单中增加「集成密钥」入口,接入应用已有的 API keys 能力。\n员工只能管理自己创建的 Key,创建时填写用途名称和有效天数。\n创建成功后只显示一次完整 Key,并提示立即复制保存。\n列表显示名称、可辨认的片段、状态、有效期和最近使用时间,支持撤销并二次确认。\n不要在列表、日志或详情中显示完整 Key。',
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
dataSources: {
|
|
142
|
+
title: '数据源',
|
|
143
|
+
description: '选择应用数据库,接入已有外部数据库中的业务数据。',
|
|
144
|
+
prompts: {
|
|
145
|
+
postgres: {
|
|
146
|
+
label: '切换数据库',
|
|
147
|
+
text: '把应用数据库从 SQLite 切换为 PostgreSQL。\n连接信息从服务端运行配置读取,不写进源码。\n说明需要安装的数据库驱动和需要填写的配置,并提醒我原有数据不会自动迁移。',
|
|
148
|
+
},
|
|
149
|
+
externalDatabase: {
|
|
150
|
+
label: '外部数据库',
|
|
151
|
+
text: '将公司已有的财务数据库作为外部数据源接入,只读取数据,不修改外部表。\n通过供应商编号匹配,在供应商详情增加「结算资料」,展示结算周期、信用额度和结算状态,并标明数据来源。\n供应商联系信息仍从应用数据库读取,结算资料直接从外部数据库读取,不导入应用数据库。\n连接凭据从服务端运行配置读取。',
|
|
152
|
+
},
|
|
135
153
|
},
|
|
136
154
|
},
|
|
137
155
|
authorization: {
|
|
@@ -146,9 +164,28 @@ const systemZhCN: SystemResource = {
|
|
|
146
164
|
label: '数据范围',
|
|
147
165
|
text: '采购申请人只查看自己申请的订单,以订单的申请人字段判断归属。\n订单审批人可以查看所有订单,处理待审批订单。\n管理员可以在权限集中分别调整两个岗位的订单查看范围。',
|
|
148
166
|
},
|
|
149
|
-
|
|
150
|
-
label: '
|
|
151
|
-
text: '
|
|
167
|
+
restrictions: {
|
|
168
|
+
label: '限制规则',
|
|
169
|
+
text: 'Bob 临时协助核对采购订单,只允许他查看金额不超过 2,000 元的订单。\n为他的「查看订单」操作增加金额上限,保留已有岗位和数据范围。\n限制同时作用于他本人的订单和共享给他的订单。\n管理员可以调整金额或移除规则;移除前说明可见范围可能扩大。',
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
workflow: {
|
|
174
|
+
title: '工作流',
|
|
175
|
+
description:
|
|
176
|
+
'让业务单据按阶段推进,处理等待、超时、失败重试和处理记录。',
|
|
177
|
+
prompts: {
|
|
178
|
+
stages: {
|
|
179
|
+
label: '流程阶段',
|
|
180
|
+
text: '为采购订单增加处理流程:待审批→待采购→已到货→已完成。\n审批人可以批准或驳回,驳回必须填写原因,订单退回申请人修改后重新提交。\n采购员下单后进入待采购,确认到货并上传签收单后进入已到货,申请人确认后完成。\n订单详情只提供当前阶段的操作,普通编辑不能直接修改流程阶段。\n在服务端检查阶段和必填信息,多人同时处理时不要覆盖别人已提交的结果。',
|
|
181
|
+
},
|
|
182
|
+
backgroundExport: {
|
|
183
|
+
label: '后台导出',
|
|
184
|
+
text: '增加订单报表页面,将现有订单导出为 CSV。\n提交后进入等待处理,由后台生成,成功后自动完成并提供下载入口。\n显示当前阶段、文件名、导出订单数,以及提交和系统处理的记录。\n生成失败时按规则重试,最终失败保留结果供负责人处理;重复执行不要生成重复的报表文件。\n刷新页面后仍可查看进度与结果,只有获得授权的人员可以导出订单。',
|
|
185
|
+
},
|
|
186
|
+
history: {
|
|
187
|
+
label: '处理记录',
|
|
188
|
+
text: '在订单详情展示当前流程阶段和处理历史。\n每条记录显示操作人、时间、操作名称、前后阶段,以及驳回原因或签收单。\n系统自动执行的动作标明由系统执行。\n刷新页面后仍保留进度和记录,完成后可以回看整个处理过程。',
|
|
152
189
|
},
|
|
153
190
|
},
|
|
154
191
|
},
|
|
@@ -184,13 +221,17 @@ const systemZhCN: SystemResource = {
|
|
|
184
221
|
title: '邮件',
|
|
185
222
|
description: '关联个人邮箱,阅读、回复和发送业务邮件。',
|
|
186
223
|
prompts: {
|
|
224
|
+
providers: {
|
|
225
|
+
label: '配置邮箱服务商',
|
|
226
|
+
text: '为应用配置 mailbox.org 邮箱接入,写入服务端配置的 mail.providers:\n- IMAP 收信服务器:imap.mailbox.org,端口 993,使用 SSL/TLS。\n- SMTP 发信服务器:smtp.mailbox.org,端口 465,使用 SSL/TLS。\n用户在邮箱账号页填写自己的邮箱地址、用户名和密码,并选择首次同步的起始日期;邮箱密码不写进配置或源码。',
|
|
227
|
+
},
|
|
187
228
|
mailCenter: {
|
|
188
229
|
label: '邮件中心',
|
|
189
|
-
text: '
|
|
230
|
+
text: '应用已内置邮件能力,请直接复用,不要重新开发或另装插件。\n在导航中增加「邮件中心」和「邮箱账号」两个页面,分别使用邮件能力提供的邮件工作台和邮箱账号页面。\n用户可以关联自己的邮箱,同步来信,阅读正文和附件,并回复邮件;回复时保留原邮件主题和上下文,收件人使用原发件人。\n每位用户只处理自己关联的邮箱。',
|
|
190
231
|
},
|
|
191
232
|
correspondence: {
|
|
192
233
|
label: '客户往来邮件',
|
|
193
|
-
text: '
|
|
234
|
+
text: '在客户详情页添加往来邮件区,复用应用内置的邮件阅读和编辑组件。\n展示发件人或收件人与当前客户联系人邮箱匹配的邮件。销售人员可以在这里阅读正文、查看附件和回复,使用自己关联的邮箱。',
|
|
194
235
|
},
|
|
195
236
|
},
|
|
196
237
|
},
|
|
@@ -208,20 +249,6 @@ const systemZhCN: SystemResource = {
|
|
|
208
249
|
},
|
|
209
250
|
},
|
|
210
251
|
},
|
|
211
|
-
templatePrint: {
|
|
212
|
-
title: '模板打印',
|
|
213
|
-
description: '把业务数据填入 Word 或 Excel 模板,生成可下载的文件。',
|
|
214
|
-
prompts: {
|
|
215
|
-
approvalForm: {
|
|
216
|
-
label: '审批单',
|
|
217
|
-
text: '在订单详情中增加“生成采购审批单”功能。\n\n使用我提供的 Word 模板,填入当前订单的编号、采购事项、申请人、金额、审批结果和处理时间。\n保留模板的标题、表格布局和签名区域,生成文件名为“采购审批单-订单编号”,提供 Word 文件下载。\n有权查看该订单的人员可以生成审批单,生成文件使用当前订单的数据。',
|
|
218
|
-
},
|
|
219
|
-
pdf: {
|
|
220
|
-
label: 'PDF 输出',
|
|
221
|
-
text: '为采购审批单增加 PDF 下载,保留 Word 模板的字体、表格和签名区域。',
|
|
222
|
-
},
|
|
223
|
-
},
|
|
224
|
-
},
|
|
225
252
|
i18n: {
|
|
226
253
|
title: '多语言',
|
|
227
254
|
description: '切换界面语言,添加新语言,设置默认语言。',
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { usePasswordResetCapability } from '@nocobase/app-plugin-authentication/client';
|
|
1
2
|
import { usePasswordResetRequest } from '@nocobase/app-plugin-authentication/client/actions';
|
|
2
3
|
import { useTranslation } from '@nocobase/i18n/client';
|
|
3
4
|
import type { ReactElement } from 'react';
|
|
@@ -6,34 +7,67 @@ import { Link } from 'react-router';
|
|
|
6
7
|
import { PasswordResetRequestForm } from '#extensions/nocobase-auth-forms/password-reset-request-form';
|
|
7
8
|
|
|
8
9
|
import { AuthPage } from './shared.js';
|
|
10
|
+
import { AuthFormStatus } from '#extensions/nocobase-auth-forms/form-parts';
|
|
11
|
+
import { Button } from '#components/ui/button';
|
|
9
12
|
|
|
10
13
|
export default function ForgotPasswordPage(): ReactElement {
|
|
11
14
|
const { t } = useTranslation();
|
|
12
15
|
const request = usePasswordResetRequest();
|
|
16
|
+
const capability = usePasswordResetCapability();
|
|
13
17
|
|
|
14
18
|
return (
|
|
15
19
|
<AuthPage
|
|
16
|
-
description={
|
|
20
|
+
description={
|
|
21
|
+
capability.data?.passwordResetAvailable === false
|
|
22
|
+
? ''
|
|
23
|
+
: t('auth.forgotDescription')
|
|
24
|
+
}
|
|
17
25
|
title={t('auth.forgotTitle')}
|
|
18
26
|
>
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
27
|
+
{capability.isPending ? (
|
|
28
|
+
<div
|
|
29
|
+
className='h-48 animate-pulse rounded-md bg-muted'
|
|
30
|
+
aria-label={t('auth.loading')}
|
|
31
|
+
/>
|
|
32
|
+
) : capability.isError ? (
|
|
33
|
+
<div className='space-y-4'>
|
|
34
|
+
<AuthFormStatus type='error'>
|
|
35
|
+
{t('auth.capabilityLoadFailed')}
|
|
36
|
+
</AuthFormStatus>
|
|
37
|
+
<Button variant='outline' onClick={() => void capability.refetch()}>
|
|
38
|
+
{t('auth.retry')}
|
|
39
|
+
</Button>
|
|
40
|
+
</div>
|
|
41
|
+
) : capability.data?.passwordResetAvailable !== true ? (
|
|
42
|
+
<div className='space-y-4'>
|
|
43
|
+
<AuthFormStatus type='error'>
|
|
44
|
+
{t('auth.passwordResetUnavailable')}
|
|
45
|
+
</AuthFormStatus>
|
|
46
|
+
<p className='text-center text-sm text-muted-foreground'>
|
|
23
47
|
{t('auth.rememberPassword')}{' '}
|
|
24
48
|
<Link to='/login'>{t('auth.signIn')}</Link>
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
49
|
+
</p>
|
|
50
|
+
</div>
|
|
51
|
+
) : (
|
|
52
|
+
<PasswordResetRequestForm
|
|
53
|
+
error={request.error?.message}
|
|
54
|
+
footer={
|
|
55
|
+
<>
|
|
56
|
+
{t('auth.rememberPassword')}{' '}
|
|
57
|
+
<Link to='/login'>{t('auth.signIn')}</Link>
|
|
58
|
+
</>
|
|
59
|
+
}
|
|
60
|
+
labels={{
|
|
61
|
+
email: t('auth.email'),
|
|
62
|
+
submit: t('auth.sendResetLink'),
|
|
63
|
+
submitting: t('auth.sending'),
|
|
64
|
+
success: t('auth.resetSent'),
|
|
65
|
+
}}
|
|
66
|
+
onSubmit={request.submit}
|
|
67
|
+
submitting={request.isPending}
|
|
68
|
+
success={request.isSuccess}
|
|
69
|
+
/>
|
|
70
|
+
)}
|
|
37
71
|
</AuthPage>
|
|
38
72
|
);
|
|
39
73
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
usePasswordResetCapability,
|
|
3
|
+
useSignUpAvailable,
|
|
4
|
+
} from '@nocobase/app-plugin-authentication/client';
|
|
2
5
|
import { usePasswordLogin } from '@nocobase/app-plugin-authentication/client/actions';
|
|
3
6
|
import { useTranslation } from '@nocobase/i18n/client';
|
|
4
7
|
import type { ReactElement } from 'react';
|
|
@@ -14,6 +17,7 @@ export default function LoginPage(): ReactElement {
|
|
|
14
17
|
const login = usePasswordLogin();
|
|
15
18
|
// The sign-up link follows the server: it is hidden while `auth.emailAndPassword` turns sign-up off.
|
|
16
19
|
const signUpAvailable = useSignUpAvailable();
|
|
20
|
+
const resetCapability = usePasswordResetCapability();
|
|
17
21
|
|
|
18
22
|
return (
|
|
19
23
|
<AuthPage
|
|
@@ -43,7 +47,9 @@ export default function LoginPage(): ReactElement {
|
|
|
43
47
|
) : undefined
|
|
44
48
|
}
|
|
45
49
|
forgotPasswordLink={
|
|
46
|
-
|
|
50
|
+
resetCapability.data?.passwordResetAvailable === true ? (
|
|
51
|
+
<Link to='/forgot-password'>{t('auth.forgotLink')}</Link>
|
|
52
|
+
) : undefined
|
|
47
53
|
}
|
|
48
54
|
labels={{
|
|
49
55
|
hidePassword: t('auth.hidePassword'),
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Bell,
|
|
3
3
|
CalendarClock,
|
|
4
|
+
Database,
|
|
4
5
|
KeyRound,
|
|
5
6
|
Languages,
|
|
6
7
|
Mail,
|
|
7
8
|
Palette,
|
|
8
9
|
Paperclip,
|
|
9
|
-
Printer,
|
|
10
10
|
ShieldCheck,
|
|
11
|
+
Workflow,
|
|
11
12
|
type LucideIcon,
|
|
12
13
|
} from 'lucide-react';
|
|
13
14
|
|
|
@@ -22,31 +23,40 @@ export interface Capability {
|
|
|
22
23
|
readonly prompts: readonly string[];
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
// The default prompts follow the examples in the documentation's Capabilities chapter.
|
|
26
|
+
// The default prompts follow the examples in the documentation's Capabilities chapter, in the order of its sidebar.
|
|
26
27
|
export const CAPABILITIES: readonly Capability[] = [
|
|
27
28
|
{
|
|
28
29
|
id: 'auth',
|
|
29
30
|
icon: KeyRound,
|
|
30
|
-
prompts: ['disableSignUp', '
|
|
31
|
+
prompts: ['disableSignUp', 'companySso', 'apiKeys'],
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: 'dataSources',
|
|
35
|
+
icon: Database,
|
|
36
|
+
prompts: ['postgres', 'externalDatabase'],
|
|
31
37
|
},
|
|
32
38
|
{
|
|
33
39
|
id: 'authorization',
|
|
34
40
|
icon: ShieldCheck,
|
|
35
|
-
prompts: ['jobs', 'dataScopes', '
|
|
41
|
+
prompts: ['jobs', 'dataScopes', 'restrictions'],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 'workflow',
|
|
45
|
+
icon: Workflow,
|
|
46
|
+
prompts: ['stages', 'backgroundExport', 'history'],
|
|
36
47
|
},
|
|
48
|
+
{ id: 'file', icon: Paperclip, prompts: ['attachments', 'avatar'] },
|
|
49
|
+
{ id: 'i18n', icon: Languages, prompts: ['addLanguage', 'defaultLanguage'] },
|
|
50
|
+
{ id: 'theme', icon: Palette, prompts: ['changeTheme', 'newTheme'] },
|
|
51
|
+
{ id: 'notification', icon: Bell, prompts: ['inApp', 'email'] },
|
|
37
52
|
{
|
|
38
53
|
id: 'scheduler',
|
|
39
54
|
icon: CalendarClock,
|
|
40
55
|
prompts: ['reminder', 'weeklyReport'],
|
|
41
56
|
},
|
|
42
|
-
{ id: 'notification', icon: Bell, prompts: ['inApp', 'email'] },
|
|
43
57
|
{
|
|
44
58
|
id: 'mail',
|
|
45
59
|
icon: Mail,
|
|
46
|
-
prompts: ['mailCenter', 'correspondence'],
|
|
60
|
+
prompts: ['providers', 'mailCenter', 'correspondence'],
|
|
47
61
|
},
|
|
48
|
-
{ id: 'file', icon: Paperclip, prompts: ['attachments', 'avatar'] },
|
|
49
|
-
{ id: 'templatePrint', icon: Printer, prompts: ['approvalForm', 'pdf'] },
|
|
50
|
-
{ id: 'i18n', icon: Languages, prompts: ['addLanguage', 'defaultLanguage'] },
|
|
51
|
-
{ id: 'theme', icon: Palette, prompts: ['changeTheme', 'newTheme'] },
|
|
52
62
|
];
|
package/client/plugins.ts
CHANGED
|
@@ -15,6 +15,7 @@ import i18n from '@nocobase/app-plugin-i18n/client';
|
|
|
15
15
|
import notification from '@nocobase/app-plugin-notification/client';
|
|
16
16
|
import scheduler from '@nocobase/app-plugin-scheduler/client';
|
|
17
17
|
import file from '@nocobase/app-plugin-file/client';
|
|
18
|
+
import mail from '@nocobase/app-plugin-mail/client';
|
|
18
19
|
|
|
19
20
|
// Array order is contribution order. A plugin is enabled by appearing in this
|
|
20
21
|
// list; removing its entry and its import disables it.
|
|
@@ -32,6 +33,7 @@ const clientPlugins: AppClientPlugins = defineClientPlugins([
|
|
|
32
33
|
notification(),
|
|
33
34
|
file(),
|
|
34
35
|
scheduler(),
|
|
36
|
+
mail(),
|
|
35
37
|
]);
|
|
36
38
|
|
|
37
39
|
export default clientPlugins;
|
package/config.example.yml
CHANGED
|
@@ -137,6 +137,11 @@ snowflake:
|
|
|
137
137
|
# scheduler:
|
|
138
138
|
# jobs: redis
|
|
139
139
|
|
|
140
|
+
# Mail is registered by default. Configure mailbox provider instances before connecting accounts.
|
|
141
|
+
# See .agents/skills/nocobase-app-plugin-mail/references/configuration-and-accounts.md.
|
|
142
|
+
mail:
|
|
143
|
+
providers: {}
|
|
144
|
+
|
|
140
145
|
# File logging is independent of terminal formatting.
|
|
141
146
|
logging:
|
|
142
147
|
level: info
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/app-template-default",
|
|
3
3
|
"displayName": "Default Template",
|
|
4
|
-
"version": "1.0.0-beta.
|
|
4
|
+
"version": "1.0.0-beta.62",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"prettier": "@nocobase/dev-config/prettier",
|
|
7
7
|
"engines": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"nocobase": {
|
|
11
11
|
"templateKind": "app",
|
|
12
|
-
"defaultTemplateVersion": "1.0.0-beta.
|
|
12
|
+
"defaultTemplateVersion": "1.0.0-beta.62"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
15
|
"@ai-sdk/react": "2.0.0",
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"tar": "^7.5.22",
|
|
78
78
|
"@nocobase/app-client": "^3.0.0-beta.0",
|
|
79
79
|
"@nocobase/app-plugin-template-print": "0.0.2-beta.1",
|
|
80
|
-
"@nocobase/app-skills": "0.1.0-beta.
|
|
80
|
+
"@nocobase/app-skills": "0.1.0-beta.30",
|
|
81
81
|
"@nocobase/app-testing": "0.1.0-beta.2",
|
|
82
82
|
"@nocobase/app-websocket": "^0.0.1",
|
|
83
83
|
"@nocobase/dev-config": "0.1.0-beta.21",
|
|
@@ -141,17 +141,18 @@
|
|
|
141
141
|
"@oclif/core": "^4.14.0",
|
|
142
142
|
"better-auth": "1.7.5",
|
|
143
143
|
"hono": "^4.13.2",
|
|
144
|
-
"@nocobase/app-plugin-authentication": "^2.0.0-beta.3",
|
|
145
144
|
"@nocobase/app-plugin-api-keys": "^1.0.0-beta.14",
|
|
145
|
+
"@nocobase/app-plugin-authentication": "^2.0.0-beta.5",
|
|
146
146
|
"@nocobase/app-plugin-authorization": "^1.0.0-beta.26",
|
|
147
147
|
"@nocobase/app-plugin-database-explorer": "^1.0.0-beta.12",
|
|
148
148
|
"@nocobase/app-plugin-file": "^1.0.0-beta.22",
|
|
149
149
|
"@nocobase/app-plugin-i18n": "^1.0.0-beta.14",
|
|
150
|
-
"@nocobase/app-plugin-
|
|
150
|
+
"@nocobase/app-plugin-mail": "^1.0.0-beta.10",
|
|
151
|
+
"@nocobase/app-plugin-notification": "^1.0.0-beta.26",
|
|
151
152
|
"@nocobase/app-plugin-notification-in-app": "^1.0.0-beta.23",
|
|
152
153
|
"@nocobase/app-plugin-notification-providers": "^0.2.0-beta.10",
|
|
153
154
|
"@nocobase/app-plugin-scheduler": "^1.0.0-beta.16",
|
|
154
|
-
"@nocobase/app-plugin-users": "^2.0.0-beta.
|
|
155
|
+
"@nocobase/app-plugin-users": "^2.0.0-beta.3",
|
|
155
156
|
"@nocobase/app-server": "^2.0.0-beta.3",
|
|
156
157
|
"@nocobase/config": "^0.1.0-beta.2",
|
|
157
158
|
"@nocobase/db": "^1.0.0-beta.18",
|
|
@@ -163,11 +164,12 @@
|
|
|
163
164
|
"@nocobase/app-plugin-authz-restriction-rules": "^1.0.0-beta.10",
|
|
164
165
|
"@nocobase/authorization": "^1.0.0-beta.12",
|
|
165
166
|
"@nocobase/queue": "^0.1.0-beta.8",
|
|
167
|
+
"@nocobase/jobs": "^0.1.0-beta.2",
|
|
168
|
+
"@nocobase/lifecycle": "^0.1.0-beta.0",
|
|
166
169
|
"@nocobase/i18n": "^1.0.0-beta.6",
|
|
167
170
|
"@nocobase/caching": "^0.1.0-beta.2",
|
|
168
171
|
"@nocobase/repository-input": "^0.1.0-beta.2",
|
|
169
|
-
"@nocobase/app-cli": "^1.0.0-beta.
|
|
170
|
-
"@nocobase/jobs": "^0.1.0-beta.2"
|
|
172
|
+
"@nocobase/app-cli": "^1.0.0-beta.18"
|
|
171
173
|
},
|
|
172
174
|
"description": "NocoBase 3 application template with the standard product capabilities.",
|
|
173
175
|
"imports": {
|
package/server/config/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
import auth from './auth.js';
|
|
6
6
|
import authorization from './authorization.js';
|
|
7
7
|
import notification from './notification.js';
|
|
8
|
+
import mail from './mail.js';
|
|
8
9
|
import secrets from './secrets.js';
|
|
9
10
|
import session from './session.js';
|
|
10
11
|
import server from './server.js';
|
|
@@ -26,6 +27,7 @@ const defaultConfigs: AppConfigFactory<{
|
|
|
26
27
|
auth: ReturnType<typeof auth>;
|
|
27
28
|
authorization: ReturnType<typeof authorization>;
|
|
28
29
|
notification: ReturnType<typeof notification>;
|
|
30
|
+
mail: ReturnType<typeof mail>;
|
|
29
31
|
secrets: ReturnType<typeof secrets>;
|
|
30
32
|
session: ReturnType<typeof session>;
|
|
31
33
|
server: ReturnType<typeof server>;
|
|
@@ -46,6 +48,7 @@ const defaultConfigs: AppConfigFactory<{
|
|
|
46
48
|
auth,
|
|
47
49
|
authorization,
|
|
48
50
|
notification,
|
|
51
|
+
mail,
|
|
49
52
|
secrets,
|
|
50
53
|
session,
|
|
51
54
|
server,
|
package/server/plugins.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from '@nocobase/app-server/plugins';
|
|
17
17
|
import scheduler from '@nocobase/app-plugin-scheduler/server';
|
|
18
18
|
import file from '@nocobase/app-plugin-file/server';
|
|
19
|
+
import mail from '@nocobase/app-plugin-mail/server';
|
|
19
20
|
|
|
20
21
|
const serverPlugins: AppServerPlugins = defineServerPlugins([
|
|
21
22
|
authentication,
|
|
@@ -32,6 +33,7 @@ const serverPlugins: AppServerPlugins = defineServerPlugins([
|
|
|
32
33
|
notificationProviders,
|
|
33
34
|
file,
|
|
34
35
|
scheduler,
|
|
36
|
+
mail,
|
|
35
37
|
]);
|
|
36
38
|
|
|
37
39
|
export default serverPlugins;
|
|
@@ -13,6 +13,12 @@ import { Loading } from '../../client/components/loading.js';
|
|
|
13
13
|
|
|
14
14
|
vi.mock('@nocobase/app-plugin-authentication/client', () => ({
|
|
15
15
|
useSignUpAvailable: () => true,
|
|
16
|
+
usePasswordResetCapability: () => ({
|
|
17
|
+
data: { passwordResetAvailable: true },
|
|
18
|
+
isPending: false,
|
|
19
|
+
isError: false,
|
|
20
|
+
refetch: vi.fn(),
|
|
21
|
+
}),
|
|
16
22
|
}));
|
|
17
23
|
|
|
18
24
|
vi.mock('@nocobase/app-plugin-authentication/client/actions', () => ({
|
|
@@ -9,6 +9,7 @@ import { describe, expect, it, vi } from 'vitest';
|
|
|
9
9
|
|
|
10
10
|
import enUS from '../../client/locales/en-US.ts';
|
|
11
11
|
import LoginPage from '../../client/pages/auth/login.tsx';
|
|
12
|
+
import ForgotPasswordPage from '../../client/pages/auth/forgot-password.tsx';
|
|
12
13
|
import RegisterPage from '../../client/pages/auth/register.tsx';
|
|
13
14
|
|
|
14
15
|
const passwordLoginAction = vi.hoisted(() => ({
|
|
@@ -18,14 +19,28 @@ const passwordLoginAction = vi.hoisted(() => ({
|
|
|
18
19
|
}));
|
|
19
20
|
|
|
20
21
|
const signUpAvailable = vi.hoisted(() => ({ value: true }));
|
|
22
|
+
const resetCapability = vi.hoisted(() => ({
|
|
23
|
+
data: { passwordResetAvailable: true } as
|
|
24
|
+
{ passwordResetAvailable: boolean } | undefined,
|
|
25
|
+
isPending: false,
|
|
26
|
+
isError: false,
|
|
27
|
+
refetch: vi.fn(),
|
|
28
|
+
}));
|
|
29
|
+
const passwordResetRequest = vi.hoisted(() => ({
|
|
30
|
+
isPending: false,
|
|
31
|
+
isSuccess: false,
|
|
32
|
+
submit: vi.fn(),
|
|
33
|
+
}));
|
|
21
34
|
|
|
22
35
|
vi.mock('@nocobase/app-plugin-authentication/client/actions', () => ({
|
|
23
36
|
usePasswordLogin: () => passwordLoginAction,
|
|
24
37
|
usePasswordRegistration: () => ({ isPending: false, submit: vi.fn() }),
|
|
38
|
+
usePasswordResetRequest: () => passwordResetRequest,
|
|
25
39
|
}));
|
|
26
40
|
|
|
27
41
|
vi.mock('@nocobase/app-plugin-authentication/client', () => ({
|
|
28
42
|
useSignUpAvailable: () => signUpAvailable.value,
|
|
43
|
+
usePasswordResetCapability: () => resetCapability,
|
|
29
44
|
}));
|
|
30
45
|
|
|
31
46
|
const runtime = await createTestI18nRuntime({
|
|
@@ -113,6 +128,68 @@ describe('application authentication pages', () => {
|
|
|
113
128
|
);
|
|
114
129
|
});
|
|
115
130
|
|
|
131
|
+
it('hides password recovery when it is not available', () => {
|
|
132
|
+
resetCapability.data = { passwordResetAvailable: false };
|
|
133
|
+
renderAt('/', <LoginPage />);
|
|
134
|
+
expect(
|
|
135
|
+
screen.queryByRole('link', { name: 'Forgot password?' }),
|
|
136
|
+
).not.toBeInTheDocument();
|
|
137
|
+
resetCapability.data = { passwordResetAvailable: true };
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('shows the administrator contact message on direct access while recovery is disabled', () => {
|
|
141
|
+
resetCapability.data = { passwordResetAvailable: false };
|
|
142
|
+
renderAt('/forgot-password', <ForgotPasswordPage />);
|
|
143
|
+
expect(screen.getByRole('alert')).toHaveTextContent(
|
|
144
|
+
'Self-service password reset is not enabled',
|
|
145
|
+
);
|
|
146
|
+
expect(screen.queryByLabelText('Email')).not.toBeInTheDocument();
|
|
147
|
+
resetCapability.data = { passwordResetAvailable: true };
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('shows loading, retries a failed capability request, and restores the form', () => {
|
|
151
|
+
resetCapability.data = undefined;
|
|
152
|
+
resetCapability.isPending = true;
|
|
153
|
+
resetCapability.isError = false;
|
|
154
|
+
const { rerender } = renderAt('/forgot-password', <ForgotPasswordPage />);
|
|
155
|
+
expect(screen.getByLabelText('Loading')).toBeVisible();
|
|
156
|
+
|
|
157
|
+
resetCapability.isPending = false;
|
|
158
|
+
resetCapability.isError = true;
|
|
159
|
+
rerender(
|
|
160
|
+
<TestI18nProvider runtime={runtime}>
|
|
161
|
+
<MemoryRouter initialEntries={['/forgot-password']}>
|
|
162
|
+
<Routes>
|
|
163
|
+
<Route element={<ForgotPasswordPage />} path='/forgot-password' />
|
|
164
|
+
</Routes>
|
|
165
|
+
</MemoryRouter>
|
|
166
|
+
</TestI18nProvider>,
|
|
167
|
+
);
|
|
168
|
+
fireEvent.click(screen.getByRole('button', { name: 'Retry' }));
|
|
169
|
+
expect(resetCapability.refetch).toHaveBeenCalled();
|
|
170
|
+
|
|
171
|
+
resetCapability.data = { passwordResetAvailable: true };
|
|
172
|
+
resetCapability.isPending = false;
|
|
173
|
+
resetCapability.isError = false;
|
|
174
|
+
passwordResetRequest.submit.mockClear();
|
|
175
|
+
rerender(
|
|
176
|
+
<TestI18nProvider runtime={runtime}>
|
|
177
|
+
<MemoryRouter initialEntries={['/forgot-password']}>
|
|
178
|
+
<Routes>
|
|
179
|
+
<Route element={<ForgotPasswordPage />} path='/forgot-password' />
|
|
180
|
+
</Routes>
|
|
181
|
+
</MemoryRouter>
|
|
182
|
+
</TestI18nProvider>,
|
|
183
|
+
);
|
|
184
|
+
fireEvent.change(screen.getByLabelText('Email'), {
|
|
185
|
+
target: { value: 'person@example.com' },
|
|
186
|
+
});
|
|
187
|
+
fireEvent.click(screen.getByRole('button', { name: 'Send reset link' }));
|
|
188
|
+
expect(passwordResetRequest.submit).toHaveBeenCalledWith({
|
|
189
|
+
email: 'person@example.com',
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
116
193
|
it('sends a visitor back to sign-in while sign-up is off', () => {
|
|
117
194
|
signUpAvailable.value = false;
|
|
118
195
|
renderAt('/register', <RegisterPage />);
|