@vertesia/ui 1.5.0 → 1.5.1

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.
Files changed (210) hide show
  1. package/lib/core/components/SelectList.d.ts.map +1 -1
  2. package/lib/core/components/SelectList.js +3 -3
  3. package/lib/core/components/SelectList.js.map +1 -1
  4. package/lib/core/components/SidePanel.d.ts.map +1 -1
  5. package/lib/core/components/SidePanel.js +2 -1
  6. package/lib/core/components/SidePanel.js.map +1 -1
  7. package/lib/features/agent/chat/ModernAgentConversation.d.ts.map +1 -1
  8. package/lib/features/agent/chat/ModernAgentConversation.js +51 -17
  9. package/lib/features/agent/chat/ModernAgentConversation.js.map +1 -1
  10. package/lib/features/agent/chat/ModernAgentOutput/MessageInput.d.ts +5 -1
  11. package/lib/features/agent/chat/ModernAgentOutput/MessageInput.d.ts.map +1 -1
  12. package/lib/features/agent/chat/ModernAgentOutput/MessageInput.js +9 -2
  13. package/lib/features/agent/chat/ModernAgentOutput/MessageInput.js.map +1 -1
  14. package/lib/features/agent/chat/hooks/useAgentPlans.d.ts +1 -1
  15. package/lib/features/agent/chat/hooks/useAgentPlans.d.ts.map +1 -1
  16. package/lib/features/agent/chat/hooks/useAgentPlans.js +5 -5
  17. package/lib/features/agent/chat/hooks/useAgentPlans.js.map +1 -1
  18. package/lib/features/magic-pdf/GroundedExtractionView.js +3 -3
  19. package/lib/features/magic-pdf/GroundedExtractionView.js.map +1 -1
  20. package/lib/features/oauth/McpConnectionsButton.d.ts +2 -2
  21. package/lib/features/oauth/McpConnectionsButton.d.ts.map +1 -1
  22. package/lib/features/oauth/McpConnectionsButton.js +9 -7
  23. package/lib/features/oauth/McpConnectionsButton.js.map +1 -1
  24. package/lib/features/oauth/McpConnectionsDialog.d.ts.map +1 -1
  25. package/lib/features/oauth/McpConnectionsDialog.js +7 -3
  26. package/lib/features/oauth/McpConnectionsDialog.js.map +1 -1
  27. package/lib/features/oauth/useMcpConnections.d.ts +4 -2
  28. package/lib/features/oauth/useMcpConnections.d.ts.map +1 -1
  29. package/lib/features/oauth/useMcpConnections.js +15 -9
  30. package/lib/features/oauth/useMcpConnections.js.map +1 -1
  31. package/lib/features/store/objects/selection/ObjectsActionContext.d.ts.map +1 -1
  32. package/lib/features/store/objects/selection/ObjectsActionContext.js +2 -0
  33. package/lib/features/store/objects/selection/ObjectsActionContext.js.map +1 -1
  34. package/lib/features/store/objects/selection/SelectionActions.d.ts +4 -2
  35. package/lib/features/store/objects/selection/SelectionActions.d.ts.map +1 -1
  36. package/lib/features/store/objects/selection/SelectionActions.js +6 -3
  37. package/lib/features/store/objects/selection/SelectionActions.js.map +1 -1
  38. package/lib/features/store/objects/selection/actions/RunIntakeAction.d.ts +8 -0
  39. package/lib/features/store/objects/selection/actions/RunIntakeAction.d.ts.map +1 -0
  40. package/lib/features/store/objects/selection/actions/RunIntakeAction.js +65 -0
  41. package/lib/features/store/objects/selection/actions/RunIntakeAction.js.map +1 -0
  42. package/lib/features/store/objects/selection/actions/index.d.ts +1 -0
  43. package/lib/features/store/objects/selection/actions/index.d.ts.map +1 -1
  44. package/lib/features/store/objects/selection/actions/index.js +1 -0
  45. package/lib/features/store/objects/selection/actions/index.js.map +1 -1
  46. package/lib/features/store/types/ContentObjectTypesTable.d.ts.map +1 -1
  47. package/lib/features/store/types/ContentObjectTypesTable.js +11 -3
  48. package/lib/features/store/types/ContentObjectTypesTable.js.map +1 -1
  49. package/lib/features/store/types/IntakePolicyEditor.d.ts.map +1 -1
  50. package/lib/features/store/types/IntakePolicyEditor.js +14 -14
  51. package/lib/features/store/types/IntakePolicyEditor.js.map +1 -1
  52. package/lib/features/store/types/TypeRegistry.d.ts +275 -150
  53. package/lib/features/store/types/TypeRegistry.d.ts.map +1 -1
  54. package/lib/features/store/types/search/ObjectTypeSearchContext.d.ts +275 -150
  55. package/lib/features/store/types/search/ObjectTypeSearchContext.d.ts.map +1 -1
  56. package/lib/i18n/locales/ar.json +28 -7
  57. package/lib/i18n/locales/de.json +24 -7
  58. package/lib/i18n/locales/en.json +24 -7
  59. package/lib/i18n/locales/es.json +24 -7
  60. package/lib/i18n/locales/fr.json +25 -7
  61. package/lib/i18n/locales/it.json +24 -7
  62. package/lib/i18n/locales/ja.json +24 -7
  63. package/lib/i18n/locales/ko.json +24 -7
  64. package/lib/i18n/locales/pt.json +24 -7
  65. package/lib/i18n/locales/ru.json +26 -7
  66. package/lib/i18n/locales/tr.json +24 -7
  67. package/lib/i18n/locales/zh-TW.json +24 -7
  68. package/lib/i18n/locales/zh.json +24 -7
  69. package/lib/shell/apps/AppInstallationProvider.d.ts +4 -4
  70. package/lib/shell/login/SignInIcons.d.ts.map +1 -1
  71. package/lib/shell/login/SignInIcons.js +2 -1
  72. package/lib/shell/login/SignInIcons.js.map +1 -1
  73. package/lib/shell/login/SignInPasswordStep.d.ts +9 -0
  74. package/lib/shell/login/SignInPasswordStep.d.ts.map +1 -0
  75. package/lib/shell/login/SignInPasswordStep.js +58 -0
  76. package/lib/shell/login/SignInPasswordStep.js.map +1 -0
  77. package/lib/shell/login/SignInPrimitives.d.ts +13 -0
  78. package/lib/shell/login/SignInPrimitives.d.ts.map +1 -1
  79. package/lib/shell/login/SignInPrimitives.js +11 -1
  80. package/lib/shell/login/SignInPrimitives.js.map +1 -1
  81. package/lib/shell/login/SignInProvidersStep.d.ts +3 -1
  82. package/lib/shell/login/SignInProvidersStep.d.ts.map +1 -1
  83. package/lib/shell/login/SignInProvidersStep.js +4 -2
  84. package/lib/shell/login/SignInProvidersStep.js.map +1 -1
  85. package/lib/shell/login/SignInReturningStep.d.ts +3 -1
  86. package/lib/shell/login/SignInReturningStep.d.ts.map +1 -1
  87. package/lib/shell/login/SignInReturningStep.js +18 -8
  88. package/lib/shell/login/SignInReturningStep.js.map +1 -1
  89. package/lib/shell/login/SignInTenantStep.d.ts +3 -1
  90. package/lib/shell/login/SignInTenantStep.d.ts.map +1 -1
  91. package/lib/shell/login/SignInTenantStep.js +2 -2
  92. package/lib/shell/login/SignInTenantStep.js.map +1 -1
  93. package/lib/shell/login/SignInWithProviderButton.d.ts +5 -3
  94. package/lib/shell/login/SignInWithProviderButton.d.ts.map +1 -1
  95. package/lib/shell/login/SignInWithProviderButton.js +8 -7
  96. package/lib/shell/login/SignInWithProviderButton.js.map +1 -1
  97. package/lib/shell/login/SigninScreen.d.ts.map +1 -1
  98. package/lib/shell/login/SigninScreen.js +13 -4
  99. package/lib/shell/login/SigninScreen.js.map +1 -1
  100. package/lib/shell/login/signInUtils.d.ts +20 -4
  101. package/lib/shell/login/signInUtils.d.ts.map +1 -1
  102. package/lib/shell/login/signInUtils.js +47 -1
  103. package/lib/shell/login/signInUtils.js.map +1 -1
  104. package/lib/vertesia-ui-core.js +1 -1
  105. package/lib/vertesia-ui-core.js.map +1 -1
  106. package/lib/vertesia-ui-features.js +14 -14
  107. package/lib/vertesia-ui-features.js.map +1 -1
  108. package/lib/vertesia-ui-i18n.BoNE1APG.chunk.js +5 -0
  109. package/lib/vertesia-ui-i18n.BoNE1APG.chunk.js.map +1 -0
  110. package/lib/vertesia-ui-i18n.BoPtiSPb.chunk.js +5 -0
  111. package/lib/vertesia-ui-i18n.BoPtiSPb.chunk.js.map +1 -0
  112. package/lib/vertesia-ui-i18n.C4BBW5v5.chunk.js +5 -0
  113. package/lib/vertesia-ui-i18n.C4BBW5v5.chunk.js.map +1 -0
  114. package/lib/vertesia-ui-i18n.C81wuNkY.chunk.js +5 -0
  115. package/lib/vertesia-ui-i18n.C81wuNkY.chunk.js.map +1 -0
  116. package/lib/vertesia-ui-i18n.C9GipKIH.chunk.js +5 -0
  117. package/lib/vertesia-ui-i18n.C9GipKIH.chunk.js.map +1 -0
  118. package/lib/vertesia-ui-i18n.CWsoS0W9.chunk.js +5 -0
  119. package/lib/vertesia-ui-i18n.CWsoS0W9.chunk.js.map +1 -0
  120. package/lib/vertesia-ui-i18n.Cr9NtzZB.chunk.js +5 -0
  121. package/lib/vertesia-ui-i18n.Cr9NtzZB.chunk.js.map +1 -0
  122. package/lib/vertesia-ui-i18n.DEpkHOD5.chunk.js +5 -0
  123. package/lib/vertesia-ui-i18n.DEpkHOD5.chunk.js.map +1 -0
  124. package/lib/vertesia-ui-i18n.DPXaRKFO.chunk.js +5 -0
  125. package/lib/vertesia-ui-i18n.DPXaRKFO.chunk.js.map +1 -0
  126. package/lib/vertesia-ui-i18n.D_QNm1Xg.chunk.js +5 -0
  127. package/lib/vertesia-ui-i18n.D_QNm1Xg.chunk.js.map +1 -0
  128. package/lib/vertesia-ui-i18n.F26Cc0f7.chunk.js +5 -0
  129. package/lib/vertesia-ui-i18n.F26Cc0f7.chunk.js.map +1 -0
  130. package/lib/vertesia-ui-i18n.YkXIESV_.chunk.js +5 -0
  131. package/lib/vertesia-ui-i18n.YkXIESV_.chunk.js.map +1 -0
  132. package/lib/vertesia-ui-i18n.js +3 -3
  133. package/lib/vertesia-ui-i18n.js.map +1 -1
  134. package/lib/vertesia-ui-shell.js +15 -9
  135. package/lib/vertesia-ui-shell.js.map +1 -1
  136. package/lib/widgets/xml-viewer/hooks/useCollapsible.d.ts +1 -1
  137. package/package.json +7 -7
  138. package/src/core/components/SelectList.tsx +5 -2
  139. package/src/core/components/SidePanel.tsx +65 -60
  140. package/src/features/agent/chat/ModernAgentConversation.test.tsx +150 -1
  141. package/src/features/agent/chat/ModernAgentConversation.tsx +59 -21
  142. package/src/features/agent/chat/ModernAgentOutput/MessageInput.tsx +17 -1
  143. package/src/features/agent/chat/hooks/useAgentPlans.test.ts +52 -0
  144. package/src/features/agent/chat/hooks/useAgentPlans.ts +5 -5
  145. package/src/features/magic-pdf/GroundedExtractionView.tsx +3 -3
  146. package/src/features/oauth/McpConnectionsButton.test.tsx +56 -0
  147. package/src/features/oauth/McpConnectionsButton.tsx +9 -6
  148. package/src/features/oauth/McpConnectionsDialog.tsx +14 -5
  149. package/src/features/oauth/useMcpConnections.test.ts +10 -0
  150. package/src/features/oauth/useMcpConnections.ts +18 -10
  151. package/src/features/store/objects/selection/ObjectsActionContext.tsx +2 -0
  152. package/src/features/store/objects/selection/SelectionActions.tsx +13 -6
  153. package/src/features/store/objects/selection/actions/RunIntakeAction.tsx +69 -0
  154. package/src/features/store/objects/selection/actions/index.ts +1 -0
  155. package/src/features/store/types/ContentObjectTypesTable.tsx +20 -3
  156. package/src/features/store/types/IntakePolicyEditor.tsx +33 -28
  157. package/src/i18n/locales/ar.json +28 -7
  158. package/src/i18n/locales/de.json +24 -7
  159. package/src/i18n/locales/en.json +24 -7
  160. package/src/i18n/locales/es.json +24 -7
  161. package/src/i18n/locales/fr.json +25 -7
  162. package/src/i18n/locales/it.json +24 -7
  163. package/src/i18n/locales/ja.json +24 -7
  164. package/src/i18n/locales/ko.json +24 -7
  165. package/src/i18n/locales/pt.json +24 -7
  166. package/src/i18n/locales/ru.json +26 -7
  167. package/src/i18n/locales/tr.json +24 -7
  168. package/src/i18n/locales/zh-TW.json +24 -7
  169. package/src/i18n/locales/zh.json +24 -7
  170. package/src/shell/login/SignInIcons.tsx +2 -1
  171. package/src/shell/login/SignInPasswordRequired.test.tsx +82 -0
  172. package/src/shell/login/SignInPasswordStep.test.tsx +71 -0
  173. package/src/shell/login/SignInPasswordStep.tsx +103 -0
  174. package/src/shell/login/SignInPrimitives.tsx +53 -1
  175. package/src/shell/login/SignInProvidersStep.tsx +8 -4
  176. package/src/shell/login/SignInReturningStep.tsx +20 -8
  177. package/src/shell/login/SignInTenantStep.tsx +6 -2
  178. package/src/shell/login/SignInWithProviderButton.tsx +11 -7
  179. package/src/shell/login/SigninScreen.tsx +32 -3
  180. package/src/shell/login/signInPassword.test.ts +133 -0
  181. package/src/shell/login/signInUtils.ts +60 -7
  182. package/lib/features/store/objects/selection/actions/StartWorkflowButton.d.ts +0 -7
  183. package/lib/features/store/objects/selection/actions/StartWorkflowButton.d.ts.map +0 -1
  184. package/lib/features/store/objects/selection/actions/StartWorkflowButton.js +0 -51
  185. package/lib/features/store/objects/selection/actions/StartWorkflowButton.js.map +0 -1
  186. package/lib/vertesia-ui-i18n.BfN7iOHb.chunk.js +0 -5
  187. package/lib/vertesia-ui-i18n.BfN7iOHb.chunk.js.map +0 -1
  188. package/lib/vertesia-ui-i18n.BjH2zEZw.chunk.js +0 -5
  189. package/lib/vertesia-ui-i18n.BjH2zEZw.chunk.js.map +0 -1
  190. package/lib/vertesia-ui-i18n.BjSwAZXl.chunk.js +0 -5
  191. package/lib/vertesia-ui-i18n.BjSwAZXl.chunk.js.map +0 -1
  192. package/lib/vertesia-ui-i18n.C7PUm5Au.chunk.js +0 -5
  193. package/lib/vertesia-ui-i18n.C7PUm5Au.chunk.js.map +0 -1
  194. package/lib/vertesia-ui-i18n.CF_1mUS5.chunk.js +0 -5
  195. package/lib/vertesia-ui-i18n.CF_1mUS5.chunk.js.map +0 -1
  196. package/lib/vertesia-ui-i18n.ChfUl3_-.chunk.js +0 -5
  197. package/lib/vertesia-ui-i18n.ChfUl3_-.chunk.js.map +0 -1
  198. package/lib/vertesia-ui-i18n.D-Sk-5d1.chunk.js +0 -5
  199. package/lib/vertesia-ui-i18n.D-Sk-5d1.chunk.js.map +0 -1
  200. package/lib/vertesia-ui-i18n.D4rnu1_S.chunk.js +0 -5
  201. package/lib/vertesia-ui-i18n.D4rnu1_S.chunk.js.map +0 -1
  202. package/lib/vertesia-ui-i18n.DDf0-laK.chunk.js +0 -5
  203. package/lib/vertesia-ui-i18n.DDf0-laK.chunk.js.map +0 -1
  204. package/lib/vertesia-ui-i18n.DN79y06J.chunk.js +0 -5
  205. package/lib/vertesia-ui-i18n.DN79y06J.chunk.js.map +0 -1
  206. package/lib/vertesia-ui-i18n.DWLIciYQ.chunk.js +0 -5
  207. package/lib/vertesia-ui-i18n.DWLIciYQ.chunk.js.map +0 -1
  208. package/lib/vertesia-ui-i18n.DvF_YaAJ.chunk.js +0 -5
  209. package/lib/vertesia-ui-i18n.DvF_YaAJ.chunk.js.map +0 -1
  210. package/src/features/store/objects/selection/actions/StartWorkflowButton.tsx +0 -104
@@ -245,6 +245,17 @@
245
245
  "auth.email.placeholder": "you@company.com",
246
246
  "auth.email.title": "欢迎使用 Vertesia",
247
247
  "auth.error": "错误:{{message}}",
248
+ "auth.password.body": "请输入此账户的密码。",
249
+ "auth.password.continue": "使用密码继续",
250
+ "auth.password.eyebrow": "密码登录",
251
+ "auth.password.failed": "无法登录,请重试。",
252
+ "auth.password.invalidCredentials": "电子邮件或密码不正确。",
253
+ "auth.password.label": "密码",
254
+ "auth.password.notAvailable": "此电子邮件不支持密码登录。",
255
+ "auth.password.required": "请输入密码。",
256
+ "auth.password.submit": "登录",
257
+ "auth.password.title": "输入密码",
258
+ "auth.password.tooManyAttempts": "尝试次数过多。请等待几分钟后重试。",
248
259
  "auth.pending.authenticating": "正在认证",
249
260
  "auth.pending.body": "请稍候 — 我们正在为您安全跳转。只需稍候片刻。",
250
261
  "auth.pending.cancel": "取消",
@@ -365,6 +376,7 @@
365
376
  "mcpConnections.label": "MCP",
366
377
  "mcpConnections.manage": "管理 MCP 连接",
367
378
  "mcpConnections.managedByApp": "由应用管理",
379
+ "mcpConnections.noAuthentication": "无需身份验证",
368
380
  "mcpConnections.settings": "设置",
369
381
  "mcpConnections.subtitle": "管理可用于此对话的 MCP 服务器。",
370
382
  "mcpConnections.title": "MCP",
@@ -474,10 +486,10 @@
474
486
  "store.actions.changeContentTypeDesc": "更改选中文档的内容类型",
475
487
  "store.actions.changeType": "更改类型",
476
488
  "store.actions.chooseExistingCollections": "从现有集合中选择",
477
- "store.actions.chooseWorkflowRule": "选择要启动的工作流规则:",
478
489
  "store.actions.confirmDeleteAll": "确定要删除所有选中的对象吗?此操作无法撤销。",
479
490
  "store.actions.confirmDeleteSelected": "确定要删除选中的对象吗?\n这不是从集合中移除。",
480
491
  "store.actions.confirmRemoveFromCollection": "确定要从此集合中移除所选对象吗?",
492
+ "store.actions.confirmRunIntake": "重新运行内容摄取会重新处理选中的对象,可能覆盖其内容类型和属性。是否继续?",
481
493
  "store.actions.createNew": "新建",
482
494
  "store.actions.delete": "删除",
483
495
  "store.actions.deleteObjects": "删除对象",
@@ -486,20 +498,22 @@
486
498
  "store.actions.errorDeletingObjects": "删除对象时出错",
487
499
  "store.actions.errorExportProperties": "导出属性时出错",
488
500
  "store.actions.errorRemovingObjects": "从集合中移除对象时出错",
489
- "store.actions.errorStartingWorkflow": "启动工作流时出错",
501
+ "store.actions.errorStartingIntake": "启动内容摄取失败",
490
502
  "store.actions.exportAllObjectProperties": "导出所有对象属性",
491
503
  "store.actions.exportProperties": "导出属性",
504
+ "store.actions.intakeStarted": "内容摄取已启动",
505
+ "store.actions.intakeStartedOn_one": "已对 {{count}} 个对象启动内容摄取",
506
+ "store.actions.intakeStartedOn_other": "已对 {{count}} 个对象启动内容摄取",
492
507
  "store.actions.noCollectionContext": "没有集合上下文",
493
508
  "store.actions.noObjectsSelected": "未选择任何对象",
494
509
  "store.actions.pleaseSelectObjectsToDelete": "请选择要删除的对象",
495
510
  "store.actions.pleaseSelectObjectsToRemove": "请选择要从集合中移除的对象",
511
+ "store.actions.pleaseSelectObjectsToRunIntake": "请选择要运行内容摄取的对象",
496
512
  "store.actions.removeFromCollection": "从集合中移除",
497
513
  "store.actions.removeFromCollectionDesc": "从此集合中移除所选对象",
514
+ "store.actions.runIntake": "运行内容摄取",
515
+ "store.actions.runIntakeDesc": "对选中的对象重新运行标准内容摄取流程",
498
516
  "store.actions.selectCollection": "选择集合",
499
- "store.actions.startWorkflow": "启动工作流",
500
- "store.actions.startWorkflowByRule": "按规则启动工作流",
501
- "store.actions.startWorkflowDesc": "对选中的对象启动工作流",
502
- "store.actions.workflowStarted": "工作流已启动",
503
517
  "store.allowedContentTypes": "允许的内容类型",
504
518
  "store.allowedContentTypesOptionalDescription": "可选择限定可添加到集合中的内容类型。如果未设置,则允许所有内容类型。",
505
519
  "store.allowedContentTypesSelectDescription": "选择可添加到集合中的内容类型。如果未设置,则允许所有内容类型。",
@@ -571,7 +585,6 @@
571
585
  "store.failedToExportDocument": "导出文档失败",
572
586
  "store.failedToFetchRoleMappings": "获取角色映射失败",
573
587
  "store.failedToFetchTypes": "获取对象类型失败",
574
- "store.failedToFetchWorkflows": "获取工作流失败",
575
588
  "store.failedToGetDownloadUrl": "获取下载URL失败",
576
589
  "store.failedToLoadDocument": "加载文档失败",
577
590
  "store.failedToLoadType": "加载类型失败",
@@ -661,6 +674,7 @@
661
674
  "store.showingFirst128K": "仅显示前128K个字符",
662
675
  "store.size": "大小",
663
676
  "store.staticCollection": "静态集合",
677
+ "store.status": "状态",
664
678
  "store.strictMode": "严格模式",
665
679
  "store.successfullyCopied": "已成功复制{{type}}到剪贴板",
666
680
  "store.successfullyExportedTo": "已成功导出为{{format}}",
@@ -675,6 +689,9 @@
675
689
  "store.typeCreated": "类型已创建",
676
690
  "store.typeDescription": "可选择内容类型以为集合分配自定义属性和数据。",
677
691
  "store.typeSelectDescription": "选择内容类型以为集合分配自定义属性和数据。",
692
+ "store.typeStatus.active": "已启用",
693
+ "store.typeStatus.draft": "草稿",
694
+ "store.typeStatus.draftHint": "摄取时不参与自动类型选择。仍可将此类型直接分配给文档。",
678
695
  "store.unknown": "未知",
679
696
  "store.unsavedChanges": "未保存的更改",
680
697
  "store.updated": "更新时间",
@@ -1,4 +1,4 @@
1
- import { LockKeyhole } from 'lucide-react';
1
+ import { KeyRound, LockKeyhole } from 'lucide-react';
2
2
  import type { ComponentType, SVGProps } from 'react';
3
3
  import type { ProviderId } from './signInUtils';
4
4
 
@@ -58,6 +58,7 @@ export const PROVIDER_ICONS: Record<ProviderId, ComponentType<{ className?: stri
58
58
  github: GithubIcon,
59
59
  microsoft: MicrosoftIcon,
60
60
  oidc: OidcIcon,
61
+ password: KeyRound,
61
62
  };
62
63
 
63
64
  export function providerIcon(provider: ProviderId | string | undefined): ComponentType<{ className?: string }> {
@@ -0,0 +1,82 @@
1
+ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
2
+ import { afterEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ const mocks = vi.hoisted(() => ({ startSignIn: vi.fn() }));
5
+
6
+ vi.mock('@vertesia/ui/i18n', () => ({
7
+ useUITranslation: () => ({ t: (key: string) => key }),
8
+ }));
9
+
10
+ vi.mock('./signInUtils', async (importOriginal) => ({
11
+ ...(await importOriginal<typeof import('./signInUtils')>()),
12
+ startSignIn: mocks.startSignIn,
13
+ }));
14
+
15
+ const { default: SignInProvidersStep } = await import('./SignInProvidersStep');
16
+ const { default: SignInReturningStep } = await import('./SignInReturningStep');
17
+
18
+ const EMAIL = 'claude@reviewers.vertesia.io';
19
+
20
+ // A provider click enters the pending screen before the redirect starts. When the address turns
21
+ // out to belong to a password tenant there is no redirect, so the step must hand over to the
22
+ // password step instead of leaving the user on the pending spinner.
23
+ describe('password-required after a provider click', () => {
24
+ afterEach(() => {
25
+ mocks.startSignIn.mockReset();
26
+ document.body.innerHTML = '';
27
+ });
28
+
29
+ it('routes the provider list to the password step', async () => {
30
+ mocks.startSignIn.mockResolvedValue({ ok: false, reason: 'password-required' });
31
+ const onPasswordRequired = vi.fn();
32
+ render(
33
+ <SignInProvidersStep
34
+ email={EMAIL}
35
+ onBack={vi.fn()}
36
+ onProviderClicked={vi.fn()}
37
+ onPasswordRequired={onPasswordRequired}
38
+ />,
39
+ );
40
+
41
+ fireEvent.click(screen.getAllByRole('button', { name: /auth.continueWithProvider/ })[0]);
42
+
43
+ await waitFor(() => expect(onPasswordRequired).toHaveBeenCalledWith(EMAIL));
44
+ });
45
+
46
+ it('routes a returning user with a stale provider to the password step', async () => {
47
+ mocks.startSignIn.mockResolvedValue({ ok: false, reason: 'password-required' });
48
+ const onProviderClicked = vi.fn();
49
+ const onPasswordRequired = vi.fn();
50
+ render(
51
+ <SignInReturningStep
52
+ session={{ email: EMAIL, lastProvider: 'google' }}
53
+ onNotYou={vi.fn()}
54
+ onProviderClicked={onProviderClicked}
55
+ onPasswordRequired={onPasswordRequired}
56
+ />,
57
+ );
58
+
59
+ fireEvent.click(screen.getByRole('button', { name: /auth.continueWithProvider/ }));
60
+
61
+ await waitFor(() => expect(onPasswordRequired).toHaveBeenCalledWith(EMAIL));
62
+ expect(onProviderClicked).toHaveBeenCalledWith('google');
63
+ });
64
+
65
+ it('does not leave the redirect path when the sign-in started', async () => {
66
+ mocks.startSignIn.mockResolvedValue({ ok: true });
67
+ const onPasswordRequired = vi.fn();
68
+ render(
69
+ <SignInReturningStep
70
+ session={{ email: EMAIL, lastProvider: 'google' }}
71
+ onNotYou={vi.fn()}
72
+ onProviderClicked={vi.fn()}
73
+ onPasswordRequired={onPasswordRequired}
74
+ />,
75
+ );
76
+
77
+ fireEvent.click(screen.getByRole('button', { name: /auth.continueWithProvider/ }));
78
+
79
+ await waitFor(() => expect(mocks.startSignIn).toHaveBeenCalledOnce());
80
+ expect(onPasswordRequired).not.toHaveBeenCalled();
81
+ });
82
+ });
@@ -0,0 +1,71 @@
1
+ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
2
+ import { afterEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ const mocks = vi.hoisted(() => ({ signInWithPassword: vi.fn() }));
5
+
6
+ vi.mock('@vertesia/ui/i18n', () => ({
7
+ useUITranslation: () => ({ t: (key: string) => key }),
8
+ }));
9
+
10
+ vi.mock('./signInUtils', () => ({ signInWithPassword: mocks.signInWithPassword }));
11
+
12
+ const { default: SignInPasswordStep } = await import('./SignInPasswordStep');
13
+
14
+ const EMAIL = 'claude@reviewers.vertesia.io';
15
+
16
+ function submitWith(password: string) {
17
+ fireEvent.change(screen.getByLabelText('auth.password.label'), { target: { value: password } });
18
+ fireEvent.click(screen.getByRole('button', { name: /auth.password.submit/ }));
19
+ }
20
+
21
+ describe('SignInPasswordStep', () => {
22
+ afterEach(() => {
23
+ mocks.signInWithPassword.mockReset();
24
+ document.body.innerHTML = '';
25
+ });
26
+
27
+ it('asks for a password before calling sign-in', () => {
28
+ render(<SignInPasswordStep email={EMAIL} onBack={vi.fn()} />);
29
+
30
+ fireEvent.click(screen.getByRole('button', { name: /auth.password.submit/ }));
31
+
32
+ expect(screen.getByRole('alert').textContent).toBe('auth.password.required');
33
+ expect(mocks.signInWithPassword).not.toHaveBeenCalled();
34
+ });
35
+
36
+ it('shows the failure and lets the user retry', async () => {
37
+ mocks.signInWithPassword.mockResolvedValue({ ok: false, reason: 'invalid-credentials' });
38
+ render(<SignInPasswordStep email={EMAIL} onBack={vi.fn()} />);
39
+
40
+ submitWith('wrong');
41
+
42
+ await waitFor(() => expect(screen.getByRole('alert').textContent).toBe('auth.password.invalidCredentials'));
43
+ expect(mocks.signInWithPassword).toHaveBeenCalledWith(EMAIL, 'wrong');
44
+ expect((screen.getByRole('button', { name: /auth.password.submit/ }) as HTMLButtonElement).disabled).toBe(
45
+ false,
46
+ );
47
+ });
48
+
49
+ // Success hands off to the session's auth-state listener, which unmounts the screen; until then
50
+ // the button must stay disabled so a second click cannot start a second sign-in.
51
+ it('keeps the submit disabled after a successful sign-in', async () => {
52
+ mocks.signInWithPassword.mockResolvedValue({ ok: true });
53
+ render(<SignInPasswordStep email={EMAIL} onBack={vi.fn()} />);
54
+
55
+ submitWith('secret');
56
+
57
+ await waitFor(() => expect(mocks.signInWithPassword).toHaveBeenCalledOnce());
58
+ const submit = screen.getAllByRole('button').find((b) => (b as HTMLButtonElement).type === 'submit');
59
+ expect((submit as HTMLButtonElement).disabled).toBe(true);
60
+ expect(screen.queryByRole('alert')).toBeNull();
61
+ });
62
+
63
+ it('returns to the email step from the change link', () => {
64
+ const onBack = vi.fn();
65
+ render(<SignInPasswordStep email={EMAIL} onBack={onBack} />);
66
+
67
+ fireEvent.click(screen.getByRole('button', { name: 'auth.change' }));
68
+
69
+ expect(onBack).toHaveBeenCalledOnce();
70
+ });
71
+ });
@@ -0,0 +1,103 @@
1
+ import { Spinner } from '@vertesia/ui/core';
2
+ import { useUITranslation } from '@vertesia/ui/i18n';
3
+ import { ArrowRight } from 'lucide-react';
4
+ import { useEffect, useRef, useState } from 'react';
5
+ import {
6
+ SignInEmailRow,
7
+ SignInPasswordField,
8
+ SignInStepButton,
9
+ SignInStepHeader,
10
+ SignInStepLayout,
11
+ } from './SignInPrimitives';
12
+ import { type PasswordSignInFailure, signInWithPassword } from './signInUtils';
13
+
14
+ interface SignInPasswordStepProps {
15
+ email: string;
16
+ tenantName?: string;
17
+ onBack: () => void;
18
+ }
19
+
20
+ /** Password sign-in for an address whose tenant issues passwords instead of federating an IdP. */
21
+ export default function SignInPasswordStep({ email, tenantName, onBack }: SignInPasswordStepProps) {
22
+ const { t } = useUITranslation();
23
+ const [password, setPassword] = useState('');
24
+ const [failure, setFailure] = useState<PasswordSignInFailure | null>(null);
25
+ const [emptyError, setEmptyError] = useState(false);
26
+ const [loading, setLoading] = useState(false);
27
+ const inputRef = useRef<HTMLInputElement>(null);
28
+
29
+ useEffect(() => {
30
+ inputRef.current?.focus();
31
+ }, []);
32
+
33
+ const submit = async (e: React.FormEvent) => {
34
+ e.preventDefault();
35
+ if (loading) return;
36
+ if (!password) {
37
+ setEmptyError(true);
38
+ return;
39
+ }
40
+ setEmptyError(false);
41
+ setFailure(null);
42
+ setLoading(true);
43
+ const result = await signInWithPassword(email, password);
44
+ if (!result.ok) {
45
+ setFailure(result.reason);
46
+ setLoading(false);
47
+ }
48
+ // On success the spinner stays up: the session's auth-state listener finishes the login and
49
+ // unmounts this screen.
50
+ };
51
+
52
+ // Literal keys, one per failure, so i18next-cli extraction sees every message.
53
+ const failureMessage = (reason: PasswordSignInFailure) => {
54
+ switch (reason) {
55
+ case 'invalid-credentials':
56
+ return t('auth.password.invalidCredentials');
57
+ case 'too-many-attempts':
58
+ return t('auth.password.tooManyAttempts');
59
+ case 'not-password-tenant':
60
+ return t('auth.password.notAvailable');
61
+ case 'failed':
62
+ return t('auth.password.failed');
63
+ }
64
+ };
65
+ const error = emptyError ? t('auth.password.required') : failure ? failureMessage(failure) : undefined;
66
+
67
+ return (
68
+ <SignInStepLayout>
69
+ <SignInStepHeader
70
+ eyebrow={tenantName ?? t('auth.password.eyebrow')}
71
+ title={t('auth.password.title')}
72
+ body={t('auth.password.body')}
73
+ />
74
+
75
+ <SignInEmailRow email={email} actionLabel={t('auth.change')} onAction={onBack} />
76
+
77
+ <form onSubmit={submit} noValidate className={`flex flex-col ${error ? 'gap-2' : 'gap-6'}`}>
78
+ <SignInPasswordField
79
+ inputRef={inputRef}
80
+ label={t('auth.password.label')}
81
+ value={password}
82
+ onChange={(value) => {
83
+ setPassword(value);
84
+ if (emptyError) setEmptyError(false);
85
+ }}
86
+ invalid={!!error}
87
+ error={error}
88
+ />
89
+
90
+ <SignInStepButton type="submit" disabled={loading}>
91
+ {loading ? (
92
+ <Spinner />
93
+ ) : (
94
+ <>
95
+ <span>{t('auth.password.submit')}</span>
96
+ <ArrowRight className="!size-3.5" />
97
+ </>
98
+ )}
99
+ </SignInStepButton>
100
+ </form>
101
+ </SignInStepLayout>
102
+ );
103
+ }
@@ -363,6 +363,11 @@ export function SignInOrDivider({ children }: { children: ReactNode }) {
363
363
  );
364
364
  }
365
365
 
366
+ const SIGNIN_INPUT_CLASS =
367
+ 'h-[42px] px-3.5 rounded-md border border-border bg-background text-sm text-foreground placeholder:text-muted ' +
368
+ 'outline-none transition focus:border-info focus:ring-4 focus:ring-info/15 ' +
369
+ 'aria-[invalid=true]:border-destructive aria-[invalid=true]:ring-destructive/15';
370
+
366
371
  interface SignInEmailFieldProps {
367
372
  inputRef?: Ref<HTMLInputElement>;
368
373
  label: ReactNode;
@@ -393,7 +398,7 @@ export function SignInEmailField({
393
398
  id="vt-login-email"
394
399
  name="vt-login-email"
395
400
  type="email"
396
- className="h-[42px] px-3.5 rounded-md border border-border bg-background text-sm text-foreground placeholder:text-muted outline-none transition focus:border-info focus:ring-4 focus:ring-info/15 aria-[invalid=true]:border-destructive aria-[invalid=true]:ring-destructive/15"
401
+ className={SIGNIN_INPUT_CLASS}
397
402
  placeholder={placeholder}
398
403
  value={value}
399
404
  onChange={(e) => onChange(e.target.value)}
@@ -414,3 +419,50 @@ export function SignInEmailField({
414
419
  </div>
415
420
  );
416
421
  }
422
+
423
+ interface SignInPasswordFieldProps {
424
+ inputRef?: Ref<HTMLInputElement>;
425
+ label: ReactNode;
426
+ value: string;
427
+ onChange: (value: string) => void;
428
+ invalid?: boolean;
429
+ error?: ReactNode;
430
+ }
431
+
432
+ /**
433
+ * Labeled password input with an inline error. Unlike the email field it leaves password managers
434
+ * enabled: a password account's credential is issued once and is expected to be stored in one.
435
+ */
436
+ export function SignInPasswordField({
437
+ inputRef,
438
+ label,
439
+ value,
440
+ onChange,
441
+ invalid = false,
442
+ error,
443
+ }: SignInPasswordFieldProps) {
444
+ return (
445
+ <div className="flex flex-col gap-1.5">
446
+ <label htmlFor="vt-login-password" className="text-xs font-medium text-foreground/80">
447
+ {label}
448
+ </label>
449
+ <input
450
+ ref={inputRef}
451
+ id="vt-login-password"
452
+ name="vt-login-password"
453
+ type="password"
454
+ className={SIGNIN_INPUT_CLASS}
455
+ value={value}
456
+ onChange={(e) => onChange(e.target.value)}
457
+ aria-invalid={invalid}
458
+ aria-describedby={error ? 'vt-login-password-error' : undefined}
459
+ autoComplete="current-password"
460
+ />
461
+ {error && (
462
+ <div id="vt-login-password-error" role="alert" className="text-xs text-destructive">
463
+ {error}
464
+ </div>
465
+ )}
466
+ </div>
467
+ );
468
+ }
@@ -1,27 +1,31 @@
1
1
  import { useUITranslation } from '@vertesia/ui/i18n';
2
2
  import { SignInEmailRow, SignInProviderButton, SignInStepHeader, SignInStepLayout } from './SignInPrimitives';
3
- import { type ProviderId, providerLabel, startSignIn } from './signInUtils';
3
+ import { type ProviderId, providerLabel, type RedirectProviderId, startSignIn } from './signInUtils';
4
4
 
5
5
  interface SignInProvidersStepProps {
6
6
  email: string;
7
7
  onBack: () => void;
8
8
  onProviderClicked: (provider: ProviderId) => void;
9
+ /** The address resolved to a password tenant on retry, so the parent shows the password step. */
10
+ onPasswordRequired: (email: string) => void;
9
11
  redirectTo?: string;
10
12
  }
11
13
 
12
- const PROVIDERS: ProviderId[] = ['google', 'github', 'microsoft'];
14
+ const PROVIDERS: RedirectProviderId[] = ['google', 'github', 'microsoft'];
13
15
 
14
16
  export default function SignInProvidersStep({
15
17
  email,
16
18
  onBack,
17
19
  onProviderClicked,
20
+ onPasswordRequired,
18
21
  redirectTo,
19
22
  }: SignInProvidersStepProps) {
20
23
  const { t } = useUITranslation();
21
24
 
22
- const pick = async (provider: ProviderId) => {
25
+ const pick = async (provider: RedirectProviderId) => {
23
26
  onProviderClicked(provider);
24
- await startSignIn(provider, email, redirectTo);
27
+ const result = await startSignIn(provider, email, redirectTo);
28
+ if (!result.ok && result.reason === 'password-required') onPasswordRequired(email);
25
29
  };
26
30
 
27
31
  return (
@@ -21,6 +21,8 @@ interface SignInReturningStepProps {
21
21
  session: LastSuccessfulLogin;
22
22
  onNotYou: () => void;
23
23
  onProviderClicked: (provider: ProviderId) => void;
24
+ /** The address signs in with a password, so the parent shows the password step instead. */
25
+ onPasswordRequired: (email: string) => void;
24
26
  redirectTo?: string;
25
27
  }
26
28
 
@@ -28,6 +30,7 @@ export default function SignInReturningStep({
28
30
  session,
29
31
  onNotYou,
30
32
  onProviderClicked,
33
+ onPasswordRequired,
31
34
  redirectTo,
32
35
  }: SignInReturningStepProps) {
33
36
  const { t } = useUITranslation();
@@ -36,14 +39,23 @@ export default function SignInReturningStep({
36
39
  const avatar = <SignInInitialsBadge initials={emailInitial(session.email)} />;
37
40
  // A stored tenantName means we resolved the user's organization.
38
41
  const hasTenant = !!session.tenantName;
42
+ const lastProvider = session.lastProvider;
39
43
  const primaryLabel =
40
- session.lastProvider === 'oidc'
41
- ? t('auth.continueWithSignIn')
42
- : t('auth.continueWithProvider', { provider: providerLabel(session.lastProvider) });
44
+ lastProvider === 'password'
45
+ ? t('auth.password.continue')
46
+ : lastProvider === 'oidc'
47
+ ? t('auth.continueWithSignIn')
48
+ : t('auth.continueWithProvider', { provider: providerLabel(lastProvider) });
43
49
 
44
- const continueWith = async (provider: ProviderId) => {
45
- onProviderClicked(provider);
46
- await startSignIn(provider, session.email, redirectTo);
50
+ const continueWithLastProvider = async () => {
51
+ if (lastProvider === 'password') {
52
+ onPasswordRequired(session.email);
53
+ return;
54
+ }
55
+ onProviderClicked(lastProvider);
56
+ // The stored provider can be stale: the address may now resolve to a password tenant.
57
+ const result = await startSignIn(lastProvider, session.email, redirectTo);
58
+ if (!result.ok && result.reason === 'password-required') onPasswordRequired(session.email);
47
59
  };
48
60
 
49
61
  return (
@@ -76,9 +88,9 @@ export default function SignInReturningStep({
76
88
 
77
89
  <div className="flex flex-col gap-2">
78
90
  <SignInProviderButton
79
- provider={session.lastProvider}
91
+ provider={lastProvider}
80
92
  label={primaryLabel}
81
- onClick={() => continueWith(session.lastProvider)}
93
+ onClick={continueWithLastProvider}
82
94
  variant="filled"
83
95
  />
84
96
  <SignInStepButton variant="ghost" onClick={onNotYou}>
@@ -8,13 +8,15 @@ import {
8
8
  SignInStepLayout,
9
9
  } from './SignInPrimitives';
10
10
  import SignInWithProviderButton from './SignInWithProviderButton';
11
- import { type ProviderId, providerLabel } from './signInUtils';
11
+ import { providerLabel, type RedirectProviderId } from './signInUtils';
12
12
 
13
13
  interface SignInTenantStepProps {
14
14
  email: string;
15
15
  tenant: TenantInfo;
16
16
  onBack: () => void;
17
17
  onProviderClicked: () => void;
18
+ /** The tenant re-resolved as a password tenant, so the parent shows the password step. */
19
+ onPasswordRequired: (email: string) => void;
18
20
  redirectTo?: string;
19
21
  }
20
22
 
@@ -32,12 +34,13 @@ export default function SignInTenantStep({
32
34
  tenant,
33
35
  onBack,
34
36
  onProviderClicked,
37
+ onPasswordRequired,
35
38
  redirectTo,
36
39
  }: SignInTenantStepProps) {
37
40
  const { t } = useUITranslation();
38
41
  const tenantName = tenant.label || tenant.name || t('auth.blocked.tenantFallback');
39
42
  // Brands keep their identity; anything else routes through OIDC.
40
- const provider: ProviderId =
43
+ const provider: RedirectProviderId =
41
44
  tenant.provider === 'google' || tenant.provider === 'github' || tenant.provider === 'microsoft'
42
45
  ? tenant.provider
43
46
  : 'oidc';
@@ -68,6 +71,7 @@ export default function SignInTenantStep({
68
71
  redirectTo={redirectTo}
69
72
  variant="filled"
70
73
  onClick={onProviderClicked}
74
+ onPasswordRequired={() => onPasswordRequired(email)}
71
75
  />
72
76
  <SignInStepButton variant="ghost" onClick={onBack}>
73
77
  {t('auth.tenant.notPartOf', { name: tenantName })}
@@ -1,15 +1,17 @@
1
1
  import { useUITranslation } from '@vertesia/ui/i18n';
2
2
  import { SignInProviderButton } from './SignInPrimitives';
3
- import { type ProviderId, providerLabel, startSignIn, startSignInWithoutTenant } from './signInUtils';
3
+ import { providerLabel, type RedirectProviderId, startSignIn, startSignInWithoutTenant } from './signInUtils';
4
4
 
5
5
  interface SignInWithProviderButtonProps {
6
- provider: ProviderId;
6
+ provider: RedirectProviderId;
7
7
  /** Email for the tenant-aware flow. Omit for a no-tenant sign-in (e.g. SignInModal). */
8
8
  email?: string;
9
9
  redirectTo?: string;
10
10
  variant?: 'outline' | 'filled';
11
11
  /** Fired on click, before the redirect. */
12
12
  onClick?: () => void;
13
+ /** Fired instead of a redirect when the email resolves to a password tenant. */
14
+ onPasswordRequired?: () => void;
13
15
  }
14
16
 
15
17
  // "Continue with <provider>" button that owns its sign-in redirect.
@@ -19,6 +21,7 @@ export default function SignInWithProviderButton({
19
21
  redirectTo,
20
22
  variant = 'outline',
21
23
  onClick,
24
+ onPasswordRequired,
22
25
  }: SignInWithProviderButtonProps) {
23
26
  const { t } = useUITranslation();
24
27
  const label =
@@ -26,15 +29,16 @@ export default function SignInWithProviderButton({
26
29
  ? t('auth.continueWithSignIn')
27
30
  : t('auth.continueWithProvider', { provider: providerLabel(provider) });
28
31
 
29
- const signIn = () => {
32
+ const signIn = async () => {
30
33
  onClick?.();
31
- if (email) {
32
- void startSignIn(provider, email, redirectTo);
33
- } else {
34
+ if (!email) {
34
35
  // OIDC needs a resolved tenant; the no-email path is only hit by no-tenant buttons.
35
36
  startSignInWithoutTenant(provider, redirectTo);
37
+ return;
36
38
  }
39
+ const result = await startSignIn(provider, email, redirectTo);
40
+ if (!result.ok && result.reason === 'password-required') onPasswordRequired?.();
37
41
  };
38
42
 
39
- return <SignInProviderButton provider={provider} label={label} onClick={signIn} variant={variant} />;
43
+ return <SignInProviderButton provider={provider} label={label} onClick={() => void signIn()} variant={variant} />;
40
44
  }