ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Frappe 前端 InviteUser 组件解析:基于 user_invitation API 的邮箱邀请面板与 useInviteUser 数据控制器

2026/9/16 18:25:58 拓冰建站 浏览量
Frappe 前端 InviteUser 组件解析:基于 user_invitation API 的邮箱邀请面板与 useInviteUser 数据控制器 Frappe 前端 InviteUser 组件解析基于 user_invitation API 的邮箱邀请面板与 useInviteUser 数据控制器【免费下载链接】frappeLow code web framework for real world applications, in Python and Javascript项目地址: https://gitcode.com/GitHub_Trending/fr/frappeInviteUser 是 Frappe 框架前端ui/目录Vue 3 frappe-ui中一个纯 UI的邮箱邀请面板它包装了 Frappe 内置的User Invitation APIfrappe.core.api.user_invitation通过多邮箱输入框支持现有用户自动补全与新地址键入、角色多选和提交按钮帮助宿主应用以零配置的方式完成通过邮箱邀请用户加入某 App的完整流程。读完本文你将掌握InviteUser组件与useInviteUser控制器的组合用法、全部 Props/Events/Slots 定制手段以及它们背后的后端权限校验、角色验证与懒加载机制。组件定位UI 与数据解耦的前后端对称设计InviteUser 的核心设计哲学是面板只管渲染、数据归宿主所有。组件源码 InviteUser.vue 的注释明确写道面板是 UI-only panel over theuseInviteUsercontroller。具体到实现上这一解耦通过两条约定落地v-bindcontroller展开控制器useInviteUser()返回的是一个 Vuereactive对象即InviteStore其数据成员roles、users、usersLoading、inviting、error等会以响应式的实时值绑定到面板其动词invite、searchUsers则作为函数 Props 传入由面板自行驱动。因为它是reactive对象官方明确告诫不要解构destructure控制器否则会丢失响应性。面板不携带标题它只渲染标准的 frappe-ui 字段英文默认文案是否加h2由宿主决定本地化、改文案或改样式统一走emailProps/rolesProps或各字段插槽见下文。模块入口 index.ts 导出了InviteUser、useInviteUser以及RoleOption、UserOption、PendingInvitation、InviteResult、UseInviteUserOptions、InviteStore、InviteUserProps等全部类型契约这些契约统一定义在 types.ts。快速上手最小可用用法以下是一个可直接运行的script setup用法示例来自 README 的 Usage 节script setup langts import { InviteUser, useInviteUser } from framework/ui/components/InviteUser; const controller useInviteUser({ appName: helpdesk, redirectPath: /helpdesk, roles: [ { label: Agent, value: Agent }, { label: Agent Manager, value: Agent Manager }, ], }); /script template h2 classtext-lg-semiboldInvite users/h2 InviteUser v-bindcontroller invited(r) console.log(invited, r.invited_emails) invalid(emails) console.warn(rejected, emails) error(e) console.error(e) / /template这段默认接线是零配置的面板自己收集邮箱与角色、调用invite、展示结果 toast 并重置表单。三个事件invited/invalid/error是给宿主挂副作用埋点、引导流程等用的而不是让宿主重写整个邀请流程——这正是动词随控制器走的体现。面板挂载即触发懒加载组件在onMounted时调用props.load?.()见 InviteUser.vue从而触发控制器的一次性懒加载。也就是说一个从未被渲染的面板不会产生任何网络请求。组件目录结构该模块的文件全部位于 ui/src/components/InviteUser/InviteUser.vue—— 面板组件本体useInviteUser.ts—— 数据控制器数据插件types.ts—— 全部 TypeScript 类型契约index.ts—— 模块导出入口tests/useInviteUser.test.ts—— 控制器单元测试Vitestmock 了frappe-ui的createResourcestories/InviteUser.story.vue—— Storybook 故事。邮箱字段experimental 的 MultiEmailInput面板的邮箱输入框使用 frappe-ui 的实验性组件MultiEmailInput从frappe-ui/experimental导入它是一个通用的chips 自动补全控件。请注意 README 中的关键限制该面板要求所安装的 frappe-ui 构建版本自带MultiEmailInput在升级之前旧版 frappe-ui 对该邮箱字段的 import 解析会失败。组件与控制器在该字段上的接线细节如下建议现有用户从Userdoctype 查询启用的、非 Website 用户的真实用户不是Contact并排除所有已经受邀到该 App 的人Pending 或 Accepted 状态。面板将字段的update:query防抖 250ms后转发给controller.searchUsers见 InviteUser.vue 中const onSearch debounce(..., 250)。下拉与键盘操作匹配用户以姓名优先于邮箱、带头像的形式展示可用鼠标或键盘选中已选地址渲染为可移除的 chips并借助 reka-ui 的TagsInput获得原生 chips 键盘导航Delete / Backspace / Arrow / Home / End。输入全新地址会通过create-labelprop 显示一行显式的Invite email选项键入的地址会经过一个务实的邮箱校验校验失败通过invalid暴露从列表选中的用户天然是合法的。在 useInviteUser.ts 中可以确认searchUsers的底层查询frappe.client.get_list作用于Userdoctype过滤条件为enabled: 1、user_type: [!, Website User]按full_name asc排序limit_page_length: 20并对name与full_name做like模糊匹配。MultiEmailInput的完整 prop/slot 面请查阅frappe-ui/experimental的文档。后端要求与约束权限、角色验证与错误分流邀请 API 的权限由每个 App 的user_invitationhookhooks.py把关谁可以邀请invite_by_email、cancel_invitation、resend_invitation、get_pending_invitations等每个方法都会调用UserInvitation.validate_role(app_name)其内部执行frappe.only_for(allowed_roles.keys())见 user_invitation.py。框架默认 hook见 hooks.py只声明了System Manager: []因此默认app_namefrappe时只有 System Manager 可以邀请。各 App 通常传入自己的appName其 hook 列出可邀请的角色集合。提供哪些角色选择器中的角色由宿主通过useInviteUser({ roles })以静态列表提供——框架已不再从 App 的 hook 推导角色列表。但在邀请时后端仍会对照该 Appuser_invitationhook 的allowed_roles校验所选角色默认frappeApp 未声明allowed_roles跳过该校验、信任宿主传入的列表。校验逻辑见UserInvitation._validate_roles()user_invitation.py对非frappeApp任何不在allowed_roles中的角色都会抛 is not an allowed role。没有角色层级框架本身没有角色层级角色会被原样插入如需要把选中的角色展开成一组角色请使用transformRoles。错误处理采用**按字段作用域field-scoped**的 frappe-ui 风格两种错误被刻意分离见 types.ts 与控制器实现 useInviteUser.tserrorinvite 专用错误仅承载invite请求的错误面板把它绑定到邮箱字段的:error上借助 frappe-ui 的useInputLabeling内联渲染在该字段下方。测试 useInviteUser.test.ts 验证了invite 错误只出现在error、不出现在loadError。loadError后台读写错误pending / already-invited 拉取、cancel、resend、用户搜索等后台请求的失败统一落到loadError刻意不挂在邮箱字段上——这样一次失败的初始加载不会把一个未触碰过的输入框标红。测试 useInviteUser.test.ts 验证了后台失败绝不上报为邮箱字段的error。如果宿主渲染了这些流程例如待处理邀请列表需要自行以 banner 或 toast 的方式展示loadError。Props 全表控制器数据 转发式定制面板通过v-bindcontroller接收控制器的数据与动词要改文案/本地化或重设样式则通过emailProps/rolesProps把 props转发到对应字段——这是一种轻路径不需要插槽、不会触发重新渲染。完整 Prop 清单如下来自 README Props 节类型契约见 types.tsProp类型默认值说明showResultToastsbooleantrue展示标准的分桶成功 toast已邀请 / 已禁用 / 已 Pending / 已 Accepted。设为false后用invited自行处理结果。emailPropsPartialMultiEmailInputProps—转发给邮箱字段MultiEmailInput的 props例如{ label, description, placeholder, size }。该字段默认没有 description需要帮助文本就传一个。rolesPropsPartialMultiSelectProps—转发给角色字段MultiSelect的 props例如{ label, placeholder }。rolesRoleOption[][]控制器数据。选择器里提供的角色来自useInviteUser({ roles })经v-bindcontroller传入。usersUserOption[][]控制器数据。邮箱 typeahead 的用户建议由searchUsers驱动。usersLoadingbooleanfalse控制器数据。用户搜索进行中为true驱动字段的 loading 态。invitingbooleanfalse控制器数据。邀请请求进行中为true禁用提交按钮并显示 spinner。errorunknownnull控制器数据。最近一次invite错误通过邮箱字段的:error内联展示。invite(emails, roles) Promise—控制器动词。发送邀请由面板的提交逻辑自动接线。searchUsers(query) void—控制器动词。获取用户建议面板把字段的update:query防抖250ms后转发给它。load() void—控制器动词。懒加载的初始请求面板挂载时调用一次。轻量定制的示例InviteUser v-bindcontroller :email-props{ label: __(Invite by email) } :roles-props{ label: __(Roles) } /三层优先级规则emailProps/rolesProps的优先级README 中明确给出在 InviteUser.vue 的模板属性顺序中同样可见面板英文默认值 你的 props 受控接线model-value、options、error、query/invalid 处理器也就是说宿主可以设置文案与外观但无法破坏字段的数据绑定。若需要对渲染的字段做完全控制请改用对应插槽见下文。另外注意提交按钮的文案是Button的插槽内容而非 prop没有可转发的 prop改名请走#submit插槽frappe-ui 内部没有 i18n字段的默认 label/placeholder 均为英文请通过emailProps/rolesProps或插槽做本地化。Events宿主副作用的三个出口EventPayload触发时机invitedInviteResult一个邀请请求成功 resolve。invalidemails: string[]一个或多个新建地址未通过邮箱校验。errorunknowninvite 动词 rejectPromise 拒绝。InviteResult是后端invite_by_email返回的分桶结果定义见 types.ts后端实现见 user_invitation.py包含四个数组invited_emails—— 本次新发出邀请的地址disabled_user_emails—— 已存在但被禁用的用户pending_invite_emails—— 已有 Pending 邀请的地址accepted_invite_emails—— 已接受邀请的地址。面板默认会为每个非空分桶各弹一条 toastInviteUser.vue 中的showResultToasts可用:show-result-toastsfalse关闭并自行消费invited。Slots重武器级的字段替换插槽是重型定制工具用于整体替换某个字段的渲染若只是改文案/外观label、placeholder、size优先使用emailProps/rolesProps。每个字段都是独立插槽因此可以只覆盖关心的字段其余保持默认每个插槽的默认内容就是标准 frappe-ui 字段不传任何插槽即可渲染标准面板。SlotProps说明email{ value, setValue, options, loading, error, search, onInvalid }替换邮箱字段。roles{ value, setValue, options }替换角色字段。submit{ submit, canSubmit, inviting }替换提交控件。插槽暴露的是实时的value加上setValue写入器而不是泄漏一个可写的 ref因此自定义字段时请绑定:model-value与update:model-value。#email作用域{ value, setValue, options, loading, error, search, onInvalid }。下面示例在保持受控接线不变的前提下本地化 labelInviteUser v-bindcontroller template #email{ value, setValue, options, loading, error, search, onInvalid } MultiEmailInput :model-valuevalue update:model-valuesetValue :label__(Invite by email) :optionsoptions :loadingloading :errorerror update:querysearch invalidonInvalid / /template /InviteUser#roles作用域{ value, setValue, options }。可把MultiSelect换成复选框组或仅改 labelInviteUser v-bindcontroller template #roles{ value, setValue, options } MultiSelect :model-valuevalue update:model-valuesetValue :label__(Access level) :optionsoptions / /template /InviteUser#submit作用域{ submit, canSubmit, inviting }。这是给按钮改名其文案是插槽内容而非 prop或重设样式的唯一途径InviteUser v-bindcontroller template #submit{ submit, canSubmit, inviting } Button variantsolid :loadinginviting :disabled!canSubmit clicksubmit {{ __(Send invitations) }} /Button /template /InviteUser三个插槽同时使用InviteUser v-bindcontroller template #email{ value, setValue, options, loading, error, search, onInvalid } MultiEmailInput :model-valuevalue update:model-valuesetValue :label__(Invite by email) :optionsoptions :loadingloading :errorerror update:querysearch invalidonInvalid / /template template #roles{ value, setValue, options } MultiSelect :model-valuevalue update:model-valuesetValue :label__(Access level) :optionsoptions / /template template #submit{ submit, canSubmit, inviting } Button variantsolid :loadinginviting :disabled!canSubmit clicksubmit {{ __(Send invitations) }} /Button /template /InviteUser注意替换#submit插槽后按钮应依据canSubmit/inviting驱动并调用submit()——这两个值编码了面板的规则至少一个邮箱且至少一个角色被选中且当前未在邀请中。由于插槽仍位于面板的form内部一个普通的typesubmit按钮同样可以工作。useInviteUser面板背后的数据插件useInviteUser是InviteUser面板背后的数据插件其实现位于 useInviteUser.ts。它的核心设计是每次调用返回全新控制器无模块级缓存旧版按appName的缓存会在用户/角色变化后变陈旧且从未被淘汰因此被移除。测试 useInviteUser.test.ts 明确验证了每次调用返回独立控制器并各自构建底层资源。懒加载创建时不发起任何请求直到调用load()面板在挂载时调用load()是幂等的每个控制器只执行一次useInviteUser.ts。测试验证了创建时不触发任何 fetch、load()恰好各 fetch 一次、重复load()是 no-op。const controller useInviteUser({ appName, // 目标 App默认 frappe redirectPath, // 被邀请者接受后落地路径默认 /app roles, // 选择器中的 RoleOption[]宿主提供邀请时后端会校验 transformRoles, // (selected) string[] — 发送前把选中的角色展开成一组 extraParams, // 额外的 invite_by_email 参数由 App 的 extra_invite_params hook 过滤 }); // controller一个 reactive 对象——不要解构 // data: pendingInvites, roles, users, loading, usersLoading, inviting, // cancellingName, resendingName, error仅 invite, loadError后台 // verbs: invite(emails, roles), cancel(name), resend(name), searchUsers(query), // load() — 懒加载初始请求幂等, reload() — 重新拉取 pending/invited控制器内部用 frappe-ui 的createResource创建了 5 个底层资源测试 useInviteUser.test.ts 验证了各自 URL 与 HTTP 方法get_pending_invitationsGET参数app_namefrappe.client.get_list查User Invitation过滤app_namestatus in [Pending, Accepted]仅取email字段用于排除已邀请者frappe.client.get_list查User用户建议运行时由searchUsers传参invite_by_emailPOSTcancel_invitationPATCH与resend_invitationPOST。invite()的请求体组装顺序值得注意useInviteUser.tsextraParams先展开核心参数emails、roles、redirect_to_path、app_name后写从而保证核心参数必然胜出——测试 useInviteUser.test.ts 专门验证了即使宿主恶意用extraParams传app_name/emails冲突键也无法把邀请重定向到别的 App。invite成功后还会reload()pending 与 already-invited 两个资源保持列表新鲜。后端白名单方法与类型清单组件所调用的白名单方法后端实现在 frappe/core/api/user_invitation.pyfrappe.core.api.user_invitation.invite_by_emailPOST—— 校验邮箱、按分桶排除已存在用户、批量插入User Invitation文档并发送邮件frappe.core.api.user_invitation.get_pending_invitationsGET—— 面板挂载时自动拉取frappe.core.api.user_invitation.cancel_invitationPATCHfrappe.core.api.user_invitation.resend_invitationPOSTfrappe.client.get_list查询User邮箱自动补全与User Invitation排除已邀请者。invite_by_email后端还会通过get_allowed_invite_paramsuser_invitation.py按 App 的extra_invite_paramshook 白名单过滤额外参数——不在白名单里的键会被忽略。被邀请者接受的完整生命周期accept_invitation→_accept→_upsert_user→after_accepthooks与 3 天过期机制INVITATION_EXPIRY_DAYS 3、mark_expired_invitations调度任务见 user_invitation.py后端集成测试覆盖于 test_user_invitation.py。模块导出index.ts的类型契约包括RoleOption、UserOption、PendingInvitation、InviteResult、UseInviteUserOptions、InviteStore、InviteUserProps、InviteEmailSlotProps、InviteRolesSlotProps、InviteSubmitSlotProps。设计权衡小结纯 UI 面板 宿主数据插件InviteUser不持有任何数据useInviteUser提供响应式控制器二者通过v-bind组合职责清晰、可独立测试与复用。有意不内置待处理邀请列表控制器仍然懒加载pendingInvitesload/reload供想自行渲染该列表的宿主使用但面板本身不提供该区块——控制面板的职责边界。无模块级缓存 懒加载每次调用useInviteUser都是全新控制器不显示则不请求避免陈旧缓存与无效请求。按字段作用域的错误模型invite 错误贴到邮箱字段下方后台读写错误走loadError绝不误伤未触碰的输入框。三层定制阶梯改文案用emailProps/rolesProps转发轻量、无重渲染整体替换用各字段插槽重型事件仅用于宿主副作用不用于重写流程。【免费下载链接】frappeLow code web framework for real world applications, in Python and Javascript项目地址: https://gitcode.com/GitHub_Trending/fr/frappe创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考